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/fritzbox.py
homeassistant/components/switch/fritzbox.py homeassistant/components/switch/fritzbox.py
homeassistant/components/ecovacs.py
homeassistant/components/*/ecovacs.py
homeassistant/components/eufy.py homeassistant/components/eufy.py
homeassistant/components/*/eufy.py homeassistant/components/*/eufy.py
@ -113,6 +116,12 @@ omit =
homeassistant/components/google.py homeassistant/components/google.py
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
homeassistant/components/*/hdmi_cec.py homeassistant/components/*/hdmi_cec.py
@ -134,11 +143,12 @@ omit =
homeassistant/components/ihc/* homeassistant/components/ihc/*
homeassistant/components/*/ihc.py homeassistant/components/*/ihc.py
homeassistant/components/insteon_local.py homeassistant/components/insteon/*
homeassistant/components/*/insteon_local.py homeassistant/components/*/insteon.py
homeassistant/components/insteon_plm/* homeassistant/components/insteon_local.py
homeassistant/components/*/insteon_plm.py
homeassistant/components/insteon_plm.py
homeassistant/components/ios.py homeassistant/components/ios.py
homeassistant/components/*/ios.py homeassistant/components/*/ios.py
@ -685,6 +695,7 @@ omit =
homeassistant/components/sensor/netdata.py homeassistant/components/sensor/netdata.py
homeassistant/components/sensor/netdata_public.py homeassistant/components/sensor/netdata_public.py
homeassistant/components/sensor/neurio_energy.py homeassistant/components/sensor/neurio_energy.py
homeassistant/components/sensor/noaa_tides.py
homeassistant/components/sensor/nsw_fuel_station.py homeassistant/components/sensor/nsw_fuel_station.py
homeassistant/components/sensor/nut.py homeassistant/components/sensor/nut.py
homeassistant/components/sensor/nzbget.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/*/bmw_connected_drive.py @ChristianKuehnel
homeassistant/components/*/broadlink.py @danielhiversen homeassistant/components/*/broadlink.py @danielhiversen
homeassistant/components/*/deconz.py @kane610 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/*/eight_sleep.py @mezz64 homeassistant/components/*/eight_sleep.py @mezz64
homeassistant/components/hive.py @Rendili @KJonline homeassistant/components/hive.py @Rendili @KJonline

View File

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

View File

@ -2,23 +2,30 @@
import asyncio import asyncio
import logging import logging
from collections import OrderedDict from collections import OrderedDict
from typing import List, Awaitable from typing import Any, Dict, List, Optional, Tuple, cast
import jwt import jwt
import voluptuous as vol
from homeassistant import data_entry_flow from homeassistant import data_entry_flow
from homeassistant.core import callback, HomeAssistant from homeassistant.core import callback, HomeAssistant
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from . import auth_store from . import auth_store, models
from .providers import auth_provider_from_config from .mfa_modules import auth_mfa_module_from_config, MultiFactorAuthModule
from .providers import auth_provider_from_config, AuthProvider, LoginFlow
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_MfaModuleDict = Dict[str, MultiFactorAuthModule]
_ProviderKey = Tuple[str, Optional[str]]
_ProviderDict = Dict[_ProviderKey, AuthProvider]
async def auth_manager_from_config( async def auth_manager_from_config(
hass: HomeAssistant, 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.""" """Initialize an auth manager from config."""
store = auth_store.AuthStore(hass) store = auth_store.AuthStore(hass)
if provider_configs: if provider_configs:
@ -26,9 +33,9 @@ async def auth_manager_from_config(
*[auth_provider_from_config(hass, store, config) *[auth_provider_from_config(hass, store, config)
for config in provider_configs]) for config in provider_configs])
else: else:
providers = [] providers = ()
# So returned auth providers are in same order as config # So returned auth providers are in same order as config
provider_hash = OrderedDict() provider_hash = OrderedDict() # type: _ProviderDict
for provider in providers: for provider in providers:
if provider is None: if provider is None:
continue continue
@ -42,28 +49,53 @@ async def auth_manager_from_config(
continue continue
provider_hash[key] = provider 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 return manager
class AuthManager: class AuthManager:
"""Manage the authentication for Home Assistant.""" """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.""" """Initialize the auth manager."""
self.hass = hass
self._store = store self._store = store
self._providers = providers self._providers = providers
self._mfa_modules = mfa_modules
self.login_flow = data_entry_flow.FlowManager( self.login_flow = data_entry_flow.FlowManager(
hass, self._async_create_login_flow, hass, self._async_create_login_flow,
self._async_finish_login_flow) self._async_finish_login_flow)
@property @property
def active(self): def active(self) -> bool:
"""Return if any auth providers are registered.""" """Return if any auth providers are registered."""
return bool(self._providers) return bool(self._providers)
@property @property
def support_legacy(self): def support_legacy(self) -> bool:
""" """
Return if legacy_api_password auth providers are registered. Return if legacy_api_password auth providers are registered.
@ -75,19 +107,39 @@ class AuthManager:
return False return False
@property @property
def auth_providers(self): def auth_providers(self) -> List[AuthProvider]:
"""Return a list of available auth providers.""" """Return a list of available auth providers."""
return list(self._providers.values()) 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.""" """Retrieve all users."""
return await self._store.async_get_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.""" """Retrieve a user."""
return await self._store.async_get_user(user_id) 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.""" """Create a system user."""
return await self._store.async_create_user( return await self._store.async_create_user(
name=name, name=name,
@ -95,27 +147,27 @@ class AuthManager:
is_active=True, is_active=True,
) )
async def async_create_user(self, name): async def async_create_user(self, name: str) -> models.User:
"""Create a user.""" """Create a user."""
kwargs = { kwargs = {
'name': name, 'name': name,
'is_active': True, 'is_active': True,
} } # type: Dict[str, Any]
if await self._user_should_be_owner(): if await self._user_should_be_owner():
kwargs['is_owner'] = True kwargs['is_owner'] = True
return await self._store.async_create_user(**kwargs) 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.""" """Get or create a user."""
if not credentials.is_new: if not credentials.is_new:
for user in await self._store.async_get_users(): user = await self.async_get_user_by_credentials(credentials)
for creds in user.credentials: if user is None:
if creds.id == credentials.id: raise ValueError('Unable to find the user.')
return user else:
return user
raise ValueError('Unable to find the user.')
auth_provider = self._async_get_auth_provider(credentials) auth_provider = self._async_get_auth_provider(credentials)
@ -127,15 +179,16 @@ class AuthManager:
return await self._store.async_create_user( return await self._store.async_create_user(
credentials=credentials, credentials=credentials,
name=info.get('name'), name=info.name,
is_active=info.get('is_active', False) 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.""" """Link credentials to an existing user."""
await self._store.async_link_user(user, credentials) 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.""" """Remove a user."""
tasks = [ tasks = [
self.async_remove_credentials(credentials) self.async_remove_credentials(credentials)
@ -147,27 +200,75 @@ class AuthManager:
await self._store.async_remove_user(user) 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.""" """Activate a user."""
await self._store.async_activate_user(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.""" """Deactivate a user."""
if user.is_owner: if user.is_owner:
raise ValueError('Unable to deactive the owner') raise ValueError('Unable to deactive the owner')
await self._store.async_deactivate_user(user) 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.""" """Remove credentials."""
provider = self._async_get_auth_provider(credentials) provider = self._async_get_auth_provider(credentials)
if (provider is not None and if (provider is not None and
hasattr(provider, 'async_will_remove_credentials')): 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) 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.""" """Create a new refresh token for a user."""
if not user.is_active: if not user.is_active:
raise ValueError('User is not active') raise ValueError('User is not active')
@ -182,16 +283,25 @@ class AuthManager:
return await self._store.async_create_refresh_token(user, client_id) 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.""" """Get refresh token by id."""
return await self._store.async_get_refresh_token(token_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.""" """Get refresh token by token."""
return await self._store.async_get_refresh_token_by_token(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 @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.""" """Create a new access token."""
# pylint: disable=no-self-use # pylint: disable=no-self-use
return jwt.encode({ return jwt.encode({
@ -200,15 +310,16 @@ class AuthManager:
'exp': dt_util.utcnow() + refresh_token.access_token_expiration, 'exp': dt_util.utcnow() + refresh_token.access_token_expiration,
}, refresh_token.jwt_key, algorithm='HS256').decode() }, refresh_token.jwt_key, algorithm='HS256').decode()
async def async_validate_access_token(self, token): async def async_validate_access_token(
"""Return if an access token is valid.""" self, token: str) -> Optional[models.RefreshToken]:
"""Return refresh token if an access token is valid."""
try: try:
unverif_claims = jwt.decode(token, verify=False) unverif_claims = jwt.decode(token, verify=False)
except jwt.InvalidTokenError: except jwt.InvalidTokenError:
return None return None
refresh_token = await self.async_get_refresh_token( refresh_token = await self.async_get_refresh_token(
unverif_claims.get('iss')) cast(str, unverif_claims.get('iss')))
if refresh_token is None: if refresh_token is None:
jwt_key = '' jwt_key = ''
@ -228,34 +339,63 @@ class AuthManager:
except jwt.InvalidTokenError: except jwt.InvalidTokenError:
return None return None
if not refresh_token.user.is_active: if refresh_token is None or not refresh_token.user.is_active:
return None return None
return refresh_token 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.""" """Create a login flow."""
auth_provider = self._providers[handler] 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): async def _async_finish_login_flow(
"""Result of a credential 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: 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']] 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']) 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 @callback
def _async_get_auth_provider(self, credentials): def _async_get_auth_provider(
"""Helper to get auth provider from a set of credentials.""" self, credentials: models.Credentials) -> Optional[AuthProvider]:
"""Get auth provider from a set of credentials."""
auth_provider_key = (credentials.auth_provider_type, auth_provider_key = (credentials.auth_provider_type,
credentials.auth_provider_id) credentials.auth_provider_id)
return self._providers.get(auth_provider_key) 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. """Determine if user should be owner.
A user should be an owner if it is the first non-system user that is 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.""" """Storage for auth models."""
from collections import OrderedDict from collections import OrderedDict
from datetime import timedelta from datetime import timedelta
from logging import getLogger
from typing import Any, Dict, List, Optional # noqa: F401
import hmac import hmac
from homeassistant.core import HomeAssistant, callback
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from . import models from . import models
@ -20,35 +23,41 @@ class AuthStore:
called that needs it. called that needs it.
""" """
def __init__(self, hass): def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the auth store.""" """Initialize the auth store."""
self.hass = hass 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) 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.""" """Retrieve all users."""
if self._users is None: if self._users is None:
await self.async_load() await self._async_load()
assert self._users is not None
return list(self._users.values()) 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.""" """Retrieve a user by id."""
if self._users is None: 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) return self._users.get(user_id)
async def async_create_user(self, name, is_owner=None, is_active=None, async def async_create_user(
system_generated=None, credentials=None): 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.""" """Create a new user."""
if self._users is None: if self._users is None:
await self.async_load() await self._async_load()
assert self._users is not None
kwargs = { kwargs = {
'name': name 'name': name
} } # type: Dict[str, Any]
if is_owner is not None: if is_owner is not None:
kwargs['is_owner'] = is_owner kwargs['is_owner'] = is_owner
@ -64,36 +73,46 @@ class AuthStore:
self._users[new_user.id] = new_user self._users[new_user.id] = new_user
if credentials is None: if credentials is None:
await self.async_save() self._async_schedule_save()
return new_user return new_user
# Saving is done inside the link. # Saving is done inside the link.
await self.async_link_user(new_user, credentials) await self.async_link_user(new_user, credentials)
return new_user 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.""" """Add credentials to an existing user."""
user.credentials.append(credentials) user.credentials.append(credentials)
await self.async_save() self._async_schedule_save()
credentials.is_new = False credentials.is_new = False
async def async_remove_user(self, user): async def async_remove_user(self, user: models.User) -> None:
"""Remove a user.""" """Remove a user."""
self._users.pop(user.id) if self._users is None:
await self.async_save() 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.""" """Activate a user."""
user.is_active = True 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.""" """Activate a user."""
user.is_active = False 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.""" """Remove credentials."""
if self._users is None:
await self._async_load()
assert self._users is not None
for user in self._users.values(): for user in self._users.values():
found = None found = None
@ -106,19 +125,35 @@ class AuthStore:
user.credentials.pop(found) user.credentials.pop(found)
break 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.""" """Create a new token for a user."""
refresh_token = models.RefreshToken(user=user, client_id=client_id) refresh_token = models.RefreshToken(user=user, client_id=client_id)
user.refresh_tokens[refresh_token.id] = refresh_token user.refresh_tokens[refresh_token.id] = refresh_token
await self.async_save() self._async_schedule_save()
return refresh_token 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.""" """Get refresh token by id."""
if self._users is None: 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(): for user in self._users.values():
refresh_token = user.refresh_tokens.get(token_id) refresh_token = user.refresh_tokens.get(token_id)
@ -127,10 +162,12 @@ class AuthStore:
return None 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.""" """Get refresh token by token."""
if self._users is None: if self._users is None:
await self.async_load() await self._async_load()
assert self._users is not None
found = None found = None
@ -141,7 +178,7 @@ class AuthStore:
return found return found
async def async_load(self): async def _async_load(self) -> None:
"""Load the users.""" """Load the users."""
data = await self._store.async_load() data = await self._store.async_load()
@ -150,7 +187,7 @@ class AuthStore:
if self._users is not None: if self._users is not None:
return return
users = OrderedDict() users = OrderedDict() # type: Dict[str, models.User]
if data is None: if data is None:
self._users = users self._users = users
@ -173,11 +210,17 @@ class AuthStore:
if 'jwt_key' not in rt_dict: if 'jwt_key' not in rt_dict:
continue 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( token = models.RefreshToken(
id=rt_dict['id'], id=rt_dict['id'],
user=users[rt_dict['user_id']], user=users[rt_dict['user_id']],
client_id=rt_dict['client_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( access_token_expiration=timedelta(
seconds=rt_dict['access_token_expiration']), seconds=rt_dict['access_token_expiration']),
token=rt_dict['token'], token=rt_dict['token'],
@ -187,8 +230,19 @@ class AuthStore:
self._users = users self._users = users
async def async_save(self): @callback
def _async_schedule_save(self) -> None:
"""Save users.""" """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 = [ users = [
{ {
'id': user.id, 'id': user.id,
@ -227,10 +281,8 @@ class AuthStore:
for refresh_token in user.refresh_tokens.values() for refresh_token in user.refresh_tokens.values()
] ]
data = { return {
'users': users, 'users': users,
'credentials': credentials, 'credentials': credentials,
'refresh_tokens': refresh_tokens, '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.""" """Auth models."""
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Dict, List, NamedTuple, Optional # noqa: F401
import uuid import uuid
import attr import attr
@ -14,17 +15,21 @@ from .util import generate_secret
class User: class User:
"""A 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)) id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
is_owner = attr.ib(type=bool, default=False) is_owner = attr.ib(type=bool, default=False)
is_active = attr.ib(type=bool, default=False) is_active = attr.ib(type=bool, default=False)
system_generated = attr.ib(type=bool, default=False) system_generated = attr.ib(type=bool, default=False)
# List of credentials of a user. # 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. # 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) @attr.s(slots=True)
@ -32,7 +37,7 @@ class RefreshToken:
"""RefreshToken for a user to grant new access tokens.""" """RefreshToken for a user to grant new access tokens."""
user = attr.ib(type=User) 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)) id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow)) created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
access_token_expiration = attr.ib(type=timedelta, access_token_expiration = attr.ib(type=timedelta,
@ -48,10 +53,14 @@ class Credentials:
"""Credentials for a user on an auth provider.""" """Credentials for a user on an auth provider."""
auth_provider_type = attr.ib(type=str) 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. # Allow the auth provider to store data to represent their auth.
data = attr.ib(type=dict) data = attr.ib(type=dict)
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
is_new = attr.ib(type=bool, default=True) 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.""" """Auth providers for Home Assistant."""
import importlib import importlib
import logging import logging
import types
from typing import Any, Dict, List, Optional
import voluptuous as vol import voluptuous as vol
from voluptuous.humanize import humanize_error from voluptuous.humanize import humanize_error
from homeassistant import requirements from homeassistant import data_entry_flow, requirements
from homeassistant.core import callback from homeassistant.core import callback, HomeAssistant
from homeassistant.const import CONF_TYPE, CONF_NAME, CONF_ID 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.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__) _LOGGER = logging.getLogger(__name__)
DATA_REQS = 'auth_prov_reqs_processed' DATA_REQS = 'auth_prov_reqs_processed'
@ -25,7 +30,87 @@ AUTH_PROVIDER_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA) }, 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.""" """Initialize an auth provider from a config."""
provider_name = config[CONF_TYPE] provider_name = config[CONF_TYPE]
module = await load_auth_provider_module(hass, provider_name) module = await load_auth_provider_module(hass, provider_name)
@ -34,16 +119,17 @@ async def auth_provider_from_config(hass, store, config):
return None return None
try: try:
config = module.CONFIG_SCHEMA(config) config = module.CONFIG_SCHEMA(config) # type: ignore
except vol.Invalid as err: except vol.Invalid as err:
_LOGGER.error('Invalid configuration for auth provider %s: %s', _LOGGER.error('Invalid configuration for auth provider %s: %s',
provider_name, humanize_error(config, err)) provider_name, humanize_error(config, err))
return None 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.""" """Load an auth provider."""
try: try:
module = importlib.import_module( module = importlib.import_module(
@ -62,8 +148,10 @@ async def load_auth_provider_module(hass, provider):
elif provider in processed: elif provider in processed:
return module return module
# https://github.com/python/mypy/issues/1424
reqs = module.REQUIREMENTS # type: ignore
req_success = await requirements.async_process_requirements( req_success = await requirements.async_process_requirements(
hass, 'auth provider {}'.format(provider), module.REQUIREMENTS) hass, 'auth provider {}'.format(provider), reqs)
if not req_success: if not req_success:
return None return None
@ -72,72 +160,88 @@ async def load_auth_provider_module(hass, provider):
return module return module
class AuthProvider: class LoginFlow(data_entry_flow.FlowHandler):
"""Provider of user authentication.""" """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): async def async_step_init(
"""Initialize an auth provider.""" self, user_input: Optional[Dict[str, str]] = None) \
self.hass = hass -> Dict[str, Any]:
self.store = store """Handle the first step of login flow.
self.config = config
@property Return self.async_show_form(step_id='init') if user_input == None.
def id(self): # pylint: disable=invalid-name Return await self.async_finish(flow_result) if login init step pass.
"""Return id of the auth provider.
Optional, can be None.
""" """
return self.config.get(CONF_ID) raise NotImplementedError
@property async def async_step_select_mfa_module(
def type(self): self, user_input: Optional[Dict[str, str]] = None) \
"""Return type of the provider.""" -> Dict[str, Any]:
return self.config[CONF_TYPE] """Handle the step of select mfa module."""
errors = {}
@property if user_input is not None:
def name(self): auth_module = user_input.get('multi_factor_auth_module')
"""Return the name of the auth provider.""" if auth_module in self.available_mfa_modules:
return self.config.get(CONF_NAME, self.DEFAULT_TITLE) self._auth_module_id = auth_module
return await self.async_step_mfa()
errors['base'] = 'invalid_auth_module'
async def async_credentials(self): if len(self.available_mfa_modules) == 1:
"""Return all credentials of this provider.""" self._auth_module_id = self.available_mfa_modules[0]
users = await self.store.async_get_users() return await self.async_step_mfa()
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 return self.async_show_form(
def async_create_credentials(self, data): step_id='select_mfa_module',
"""Create credentials.""" data_schema=vol.Schema({
return Credentials( 'multi_factor_auth_module': vol.In(self.available_mfa_modules)
auth_provider_type=self.type, }),
auth_provider_id=self.id, errors=errors,
data=data,
) )
# 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): auth_module = self._auth_manager.get_auth_mfa_module(
"""Return the data flow for logging in with auth provider.""" self._auth_module_id)
raise NotImplementedError 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): if user_input is not None:
"""Get credentials based on the flow result.""" expires = self.created_at + SESSION_EXPIRATION
raise NotImplementedError 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): if not errors:
"""Return extra user metadata for credentials. 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: async def async_finish(self, flow_result: Any) -> Dict:
- name: string """Handle the pass of login flow."""
- is_active: boolean return self.async_create_entry(
""" title=self._auth_provider.name,
return {} data=flow_result
)

View File

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

View File

@ -1,14 +1,15 @@
"""Example auth provider.""" """Example auth provider."""
from collections import OrderedDict from collections import OrderedDict
import hmac import hmac
from typing import Any, Dict, Optional, cast
import voluptuous as vol import voluptuous as vol
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant import data_entry_flow
from homeassistant.core import callback 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({ USER_SCHEMA = vol.Schema({
@ -31,13 +32,13 @@ class InvalidAuthError(HomeAssistantError):
class ExampleAuthProvider(AuthProvider): class ExampleAuthProvider(AuthProvider):
"""Example auth provider based on hardcoded usernames and passwords.""" """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 a flow to login."""
return LoginFlow(self) return ExampleLoginFlow(self)
@callback @callback
def async_validate_login(self, username, password): def async_validate_login(self, username: str, password: str) -> None:
"""Helper to validate a username and password.""" """Validate a username and password."""
user = None user = None
# Compare all users to avoid timing attacks. # Compare all users to avoid timing attacks.
@ -56,7 +57,8 @@ class ExampleAuthProvider(AuthProvider):
password.encode('utf-8')): password.encode('utf-8')):
raise InvalidAuthError 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.""" """Get credentials based on the flow result."""
username = flow_result['username'] username = flow_result['username']
@ -69,49 +71,45 @@ class ExampleAuthProvider(AuthProvider):
'username': username '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. """Return extra user metadata for credentials.
Will be used to populate info when creating a new user. Will be used to populate info when creating a new user.
""" """
username = credentials.data['username'] username = credentials.data['username']
info = { name = None
'is_active': True,
}
for user in self.config['users']: for user in self.config['users']:
if user['username'] == username: if user['username'] == username:
info['name'] = user.get('name') name = user.get('name')
break break
return info return UserMeta(name=name, is_active=True)
class LoginFlow(data_entry_flow.FlowHandler): class ExampleLoginFlow(LoginFlow):
"""Handler for the login flow.""" """Handler for the login flow."""
def __init__(self, auth_provider): async def async_step_init(
"""Initialize the login flow.""" self, user_input: Optional[Dict[str, str]] = None) \
self._auth_provider = auth_provider -> Dict[str, Any]:
async def async_step_init(self, user_input=None):
"""Handle the step of the form.""" """Handle the step of the form."""
errors = {} errors = {}
if user_input is not None: if user_input is not None:
try: try:
self._auth_provider.async_validate_login( cast(ExampleAuthProvider, self._auth_provider)\
user_input['username'], user_input['password']) .async_validate_login(user_input['username'],
user_input['password'])
except InvalidAuthError: except InvalidAuthError:
errors['base'] = 'invalid_auth' errors['base'] = 'invalid_auth'
if not errors: if not errors:
return self.async_create_entry( user_input.pop('password')
title=self._auth_provider.name, return await self.async_finish(user_input)
data=user_input
)
schema = OrderedDict() schema = OrderedDict() # type: Dict[str, type]
schema['username'] = str schema['username'] = str
schema['password'] = 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 It will be removed when auth system production ready
""" """
from collections import OrderedDict
import hmac import hmac
from typing import Any, Dict, Optional, cast
import voluptuous as vol import voluptuous as vol
from homeassistant.exceptions import HomeAssistantError from homeassistant.components.http import HomeAssistantHTTP # noqa: F401
from homeassistant import data_entry_flow
from homeassistant.core import callback 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({ USER_SCHEMA = vol.Schema({
@ -36,25 +37,21 @@ class LegacyApiPasswordAuthProvider(AuthProvider):
DEFAULT_TITLE = 'Legacy API Password' 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 a flow to login."""
return LoginFlow(self) return LegacyLoginFlow(self)
@callback @callback
def async_validate_login(self, password): def async_validate_login(self, password: str) -> None:
"""Helper to validate a username and password.""" """Validate a username and password."""
if not hasattr(self.hass, 'http'): hass_http = getattr(self.hass, 'http', None) # type: HomeAssistantHTTP
raise ValueError('http component is not loaded')
if self.hass.http.api_password is None: if not hmac.compare_digest(hass_http.api_password.encode('utf-8'),
raise ValueError('http component is not configured using'
' api_password')
if not hmac.compare_digest(self.hass.http.api_password.encode('utf-8'),
password.encode('utf-8')): password.encode('utf-8')):
raise InvalidAuthError 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.""" """Return LEGACY_USER always."""
for credential in await self.async_credentials(): for credential in await self.async_credentials():
if credential.data['username'] == LEGACY_USER: if credential.data['username'] == LEGACY_USER:
@ -64,47 +61,43 @@ class LegacyApiPasswordAuthProvider(AuthProvider):
'username': LEGACY_USER '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. Set name as LEGACY_USER always.
Will be used to populate info when creating a new user. Will be used to populate info when creating a new user.
""" """
return { return UserMeta(name=LEGACY_USER, is_active=True)
'name': LEGACY_USER,
'is_active': True,
}
class LoginFlow(data_entry_flow.FlowHandler): class LegacyLoginFlow(LoginFlow):
"""Handler for the login flow.""" """Handler for the login flow."""
def __init__(self, auth_provider): async def async_step_init(
"""Initialize the login flow.""" self, user_input: Optional[Dict[str, str]] = None) \
self._auth_provider = auth_provider -> Dict[str, Any]:
async def async_step_init(self, user_input=None):
"""Handle the step of the form.""" """Handle the step of the form."""
errors = {} 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: if user_input is not None:
try: try:
self._auth_provider.async_validate_login( cast(LegacyApiPasswordAuthProvider, self._auth_provider)\
user_input['password']) .async_validate_login(user_input['password'])
except InvalidAuthError: except InvalidAuthError:
errors['base'] = 'invalid_auth' errors['base'] = 'invalid_auth'
if not errors: if not errors:
return self.async_create_entry( return await self.async_finish({})
title=self._auth_provider.name,
data={}
)
schema = OrderedDict()
schema['password'] = str
return self.async_show_form( return self.async_show_form(
step_id='init', step_id='init',
data_schema=vol.Schema(schema), data_schema=vol.Schema({'password': str}),
errors=errors, errors=errors,
) )

View File

@ -3,12 +3,16 @@
It shows list of users if access from trusted network. It shows list of users if access from trusted network.
Abort login flow if not access from trusted network. Abort login flow if not access from trusted network.
""" """
from typing import Any, Dict, Optional, cast
import voluptuous as vol 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.core import callback
from homeassistant.exceptions import HomeAssistantError 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({ CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
}, extra=vol.PREVENT_EXTRA) }, extra=vol.PREVENT_EXTRA)
@ -31,16 +35,24 @@ class TrustedNetworksAuthProvider(AuthProvider):
DEFAULT_TITLE = 'Trusted Networks' 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.""" """Return a flow to login."""
assert context is not None
users = await self.store.async_get_users() users = await self.store.async_get_users()
available_users = {user.id: user.name available_users = {user.id: user.name
for user in users for user in users
if not user.system_generated and user.is_active} 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.""" """Get credentials based on the flow result."""
user_id = flow_result['user'] user_id = flow_result['user']
@ -59,7 +71,8 @@ class TrustedNetworksAuthProvider(AuthProvider):
# We only allow login as exist user # We only allow login as exist user
raise InvalidUserError 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. """Return extra user metadata for credentials.
Trusted network auth provider should never create new user. Trusted network auth provider should never create new user.
@ -67,35 +80,41 @@ class TrustedNetworksAuthProvider(AuthProvider):
raise NotImplementedError raise NotImplementedError
@callback @callback
def async_validate_access(self, ip_address): def async_validate_access(self, ip_address: str) -> None:
"""Make sure the access from trusted networks. """Make sure the access from trusted networks.
Raise InvalidAuthError if not. 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 hass_http = getattr(self.hass, 'http', None) # type: HomeAssistantHTTP
not self.hass.http or not self.hass.http.trusted_networks):
if not hass_http or not hass_http.trusted_networks:
raise InvalidAuthError('trusted_networks is not configured') raise InvalidAuthError('trusted_networks is not configured')
if not any(ip_address in trusted_network for trusted_network 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') raise InvalidAuthError('Not in trusted_networks')
class LoginFlow(data_entry_flow.FlowHandler): class TrustedNetworksLoginFlow(LoginFlow):
"""Handler for the login flow.""" """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.""" """Initialize the login flow."""
self._auth_provider = auth_provider super().__init__(auth_provider)
self._available_users = available_users self._available_users = available_users
self._ip_address = ip_address 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.""" """Handle the step of the form."""
errors = {} errors = {}
try: try:
self._auth_provider.async_validate_access(self._ip_address) cast(TrustedNetworksAuthProvider, self._auth_provider)\
.async_validate_access(self._ip_address)
except InvalidAuthError: except InvalidAuthError:
errors['base'] = 'invalid_auth' errors['base'] = 'invalid_auth'
@ -111,10 +130,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
errors['base'] = 'invalid_auth' errors['base'] = 'invalid_auth'
if not errors: if not errors:
return self.async_create_entry( return await self.async_finish(user_input)
title=self._auth_provider.name,
data=user_input
)
schema = {'user': vol.In(self._available_users)} 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) log_no_color)
core_config = config.get(core.DOMAIN, {}) core_config = config.get(core.DOMAIN, {})
has_api_password = bool((config.get('http') or {}).get('api_password'))
try: 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: except vol.Invalid as ex:
conf_util.async_log_exception(ex, 'homeassistant', core_config, hass) conf_util.async_log_exception(ex, 'homeassistant', core_config, hass)
return None return None
@ -307,7 +309,7 @@ def async_enable_logging(hass: core.HomeAssistant,
hass.data[DATA_LOGGING] = err_log_path hass.data[DATA_LOGGING] = err_log_path
else: else:
_LOGGER.error( _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: 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): 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) 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/ https://home-assistant.io/components/alarm_control_panel.homematicip_cloud/
""" """
import logging 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 ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED) 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__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['homematicip_cloud']
HMIP_ZONE_AWAY = 'EXTERNAL' HMIP_ZONE_AWAY = 'EXTERNAL'
HMIP_ZONE_HOME = 'INTERNAL' HMIP_ZONE_HOME = 'INTERNAL'
async def async_setup_platform(hass, config, async_add_devices, async def async_setup_platform(
discovery_info=None): hass, config, async_add_devices, discovery_info=None):
"""Set up the HomematicIP alarm control devices.""" """Set up the HomematicIP Cloud alarm control devices."""
pass pass
@ -45,7 +44,7 @@ async def async_setup_entry(hass, config_entry, async_add_devices):
class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel): class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel):
"""Representation of an HomematicIP security zone group.""" """Representation of an HomematicIP Cloud security zone group."""
def __init__(self, home, device): def __init__(self, home, device):
"""Initialize the security zone group.""" """Initialize the security zone group."""

View File

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

View File

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

View File

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

View File

@ -44,13 +44,26 @@ a limited expiration.
"expires_in": 1800, "expires_in": 1800,
"token_type": "Bearer" "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 logging
import uuid import uuid
from datetime import timedelta from datetime import timedelta
from aiohttp import web
import voluptuous as vol import voluptuous as vol
from homeassistant.auth.models import User, Credentials
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.components.http.ban import log_invalid_auth from homeassistant.components.http.ban import log_invalid_auth
from homeassistant.components.http.data_validator import RequestDataValidator 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, vol.Required('type'): WS_TYPE_CURRENT_USER,
}) })
RESULT_TYPE_CREDENTIALS = 'credentials'
RESULT_TYPE_USER = 'user'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def async_setup(hass, config): async def async_setup(hass, config):
"""Component to allow users to login.""" """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(TokenView(retrieve_result))
hass.http.register_view(LinkUserView(retrieve_credentials)) hass.http.register_view(LinkUserView(retrieve_result))
hass.components.websocket_api.async_register_command( hass.components.websocket_api.async_register_command(
WS_TYPE_CURRENT_USER, websocket_current_user, WS_TYPE_CURRENT_USER, websocket_current_user,
SCHEMA_WS_CURRENT_USER SCHEMA_WS_CURRENT_USER
) )
await login_flow.async_setup(hass, store_credentials) await login_flow.async_setup(hass, store_result)
return True return True
class GrantTokenView(HomeAssistantView): class TokenView(HomeAssistantView):
"""View to grant tokens.""" """View to issue or revoke tokens."""
url = '/auth/token' url = '/auth/token'
name = 'api:auth:token' name = 'api:auth:token'
requires_auth = False requires_auth = False
cors_allowed = True cors_allowed = True
def __init__(self, retrieve_credentials): def __init__(self, retrieve_user):
"""Initialize the grant token view.""" """Initialize the token view."""
self._retrieve_credentials = retrieve_credentials self._retrieve_user = retrieve_user
@log_invalid_auth @log_invalid_auth
async def post(self, request): async def post(self, request):
@ -108,6 +124,13 @@ class GrantTokenView(HomeAssistantView):
grant_type = data.get('grant_type') 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': if grant_type == 'authorization_code':
return await self._async_handle_auth_code(hass, data) return await self._async_handle_auth_code(hass, data)
@ -118,6 +141,25 @@ class GrantTokenView(HomeAssistantView):
'error': 'unsupported_grant_type', 'error': 'unsupported_grant_type',
}, status_code=400) }, 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): async def _async_handle_auth_code(self, hass, data):
"""Handle authorization code request.""" """Handle authorization code request."""
client_id = data.get('client_id') client_id = data.get('client_id')
@ -132,17 +174,19 @@ class GrantTokenView(HomeAssistantView):
if code is None: if code is None:
return self.json({ return self.json({
'error': 'invalid_request', 'error': 'invalid_request',
'error_description': 'Invalid code',
}, status_code=400) }, 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({ return self.json({
'error': 'invalid_request', 'error': 'invalid_request',
'error_description': 'Invalid code', 'error_description': 'Invalid code',
}, status_code=400) }, 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: if not user.is_active:
return self.json({ return self.json({
@ -220,7 +264,7 @@ class LinkUserView(HomeAssistantView):
user = request['hass_user'] user = request['hass_user']
credentials = self._retrieve_credentials( credentials = self._retrieve_credentials(
data['client_id'], data['code']) data['client_id'], RESULT_TYPE_CREDENTIALS, data['code'])
if credentials is None: if credentials is None:
return self.json_message('Invalid code', status_code=400) return self.json_message('Invalid code', status_code=400)
@ -230,37 +274,45 @@ class LinkUserView(HomeAssistantView):
@callback @callback
def _create_cred_store(): def _create_auth_code_store():
"""Create a credential store.""" """Create an in memory store."""
temp_credentials = {} temp_results = {}
@callback @callback
def store_credentials(client_id, credentials): def store_result(client_id, result):
"""Store credentials and return a code to retrieve it.""" """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 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 return code
@callback @callback
def retrieve_credentials(client_id, code): def retrieve_result(client_id, result_type, code):
"""Retrieve credentials.""" """Retrieve flow result."""
key = (client_id, code) key = (client_id, result_type, code)
if key not in temp_credentials: if key not in temp_results:
return None return None
created, credentials = temp_credentials.pop(key) created, _, result = temp_results.pop(key)
# OAuth 4.2.1 # OAuth 4.2.1
# The authorization code MUST expire shortly after it is issued to # The authorization code MUST expire shortly after it is issued to
# mitigate the risk of leaks. A maximum authorization code lifetime of # mitigate the risk of leaks. A maximum authorization code lifetime of
# 10 minutes is RECOMMENDED. # 10 minutes is RECOMMENDED.
if dt_util.utcnow() - created < timedelta(minutes=10): if dt_util.utcnow() - created < timedelta(minutes=10):
return credentials return result
return None return None
return store_credentials, retrieve_credentials return store_result, retrieve_result
@callback @callback

View File

@ -4,6 +4,7 @@ from html.parser import HTMLParser
from ipaddress import ip_address, ip_network from ipaddress import ip_address, ip_network
from urllib.parse import urlparse, urljoin from urllib.parse import urlparse, urljoin
import aiohttp
from aiohttp.client_exceptions import ClientError from aiohttp.client_exceptions import ClientError
# IP addresses of loopback interfaces # 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. We do not implement extracting redirect uris from headers.
""" """
session = hass.helpers.aiohttp_client.async_get_clientsession()
parser = LinkTagParser('redirect_uri') parser = LinkTagParser('redirect_uri')
chunks = 0 chunks = 0
try: 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): if chunks == 10:
parser.feed(data.decode()) break
chunks += 1
if chunks == 10:
break
except (asyncio.TimeoutError, ClientError): except (asyncio.TimeoutError, ClientError):
pass 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 Pass in parameter 'handler' to specify the auth provider to use. Auth providers
are identified by type and id. 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/", "client_id": "https://hassbian.local:8123/",
"handler": ["local_provider", null], "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 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 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 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. 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", "flow_id": "8f7e42faab604bcab7ac43c44ca34d58",
@ -71,12 +78,12 @@ from homeassistant.components.http.view import HomeAssistantView
from . import indieauth from . import indieauth
async def async_setup(hass, store_credentials): async def async_setup(hass, store_result):
"""Component to allow users to login.""" """Component to allow users to login."""
hass.http.register_view(AuthProvidersView) hass.http.register_view(AuthProvidersView)
hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow)) hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow))
hass.http.register_view( hass.http.register_view(
LoginFlowResourceView(hass.auth.login_flow, store_credentials)) LoginFlowResourceView(hass.auth.login_flow, store_result))
class AuthProvidersView(HomeAssistantView): class AuthProvidersView(HomeAssistantView):
@ -138,6 +145,7 @@ class LoginFlowIndexView(HomeAssistantView):
vol.Required('client_id'): str, vol.Required('client_id'): str,
vol.Required('handler'): vol.Any(str, list), vol.Required('handler'): vol.Any(str, list),
vol.Required('redirect_uri'): str, vol.Required('redirect_uri'): str,
vol.Optional('type', default='authorize'): str,
})) }))
@log_invalid_auth @log_invalid_auth
async def post(self, request, data): async def post(self, request, data):
@ -153,7 +161,10 @@ class LoginFlowIndexView(HomeAssistantView):
try: try:
result = await self._flow_mgr.async_init( 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: except data_entry_flow.UnknownHandler:
return self.json_message('Invalid handler specified', 404) return self.json_message('Invalid handler specified', 404)
except data_entry_flow.UnknownStep: except data_entry_flow.UnknownStep:
@ -169,10 +180,10 @@ class LoginFlowResourceView(HomeAssistantView):
name = 'api:auth:login_flow:resource' name = 'api:auth:login_flow:resource'
requires_auth = False requires_auth = False
def __init__(self, flow_mgr, store_credentials): def __init__(self, flow_mgr, store_result):
"""Initialize the login flow resource view.""" """Initialize the login flow resource view."""
self._flow_mgr = flow_mgr self._flow_mgr = flow_mgr
self._store_credentials = store_credentials self._store_result = store_result
async def get(self, request): async def get(self, request):
"""Do not allow getting status of a flow in progress.""" """Do not allow getting status of a flow in progress."""
@ -212,7 +223,7 @@ class LoginFlowResourceView(HomeAssistantView):
return self.json(_prepare_result_json(result)) return self.json(_prepare_result_json(result))
result.pop('data') 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) 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 For more details about this component, please refer to the documentation at
https://home-assistant.io/components/automation/ 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): 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) return await hass.data[DOMAIN].async_setup_entry(entry)

View File

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

View File

@ -58,7 +58,7 @@ class EgardiaBinarySensor(BinarySensorDevice):
@property @property
def name(self): def name(self):
"""The name of the device.""" """Return the name of the device."""
return self._name return self._name
@property @property
@ -74,5 +74,5 @@ class EgardiaBinarySensor(BinarySensorDevice):
@property @property
def device_class(self): def device_class(self):
"""The device class.""" """Return the device class."""
return self._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) data = HikvisionData(hass, url, port, name, username, password)
if data.sensors is None: 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 return False
entities = [] 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/ https://home-assistant.io/components/binary_sensor.homematic/
""" """
import logging import logging
from homeassistant.const import STATE_UNKNOWN
from homeassistant.components.binary_sensor import BinarySensorDevice 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__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['homematic'] DEPENDENCIES = ['homematic']
SENSOR_TYPES_CLASS = { SENSOR_TYPES_CLASS = {
'Remote': None,
'ShutterContact': 'opening',
'MaxShutterContact': 'opening',
'IPShutterContact': 'opening', 'IPShutterContact': 'opening',
'Smoke': 'smoke', 'MaxShutterContact': 'opening',
'SmokeV2': 'smoke',
'Motion': 'motion', 'Motion': 'motion',
'MotionV2': 'motion', 'MotionV2': 'motion',
'RemoteMotion': None,
'WeatherSensor': None,
'TiltSensor': None,
'PresenceIP': 'motion', '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/ https://home-assistant.io/components/binary_sensor.homematicip_cloud/
""" """
import logging import logging
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.homematicip_cloud import ( from homeassistant.components.homematicip_cloud import (
HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice)
HMIPC_HAPID) from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN
DEPENDENCIES = ['homematicip_cloud'] DEPENDENCIES = ['homematicip_cloud']
@ -19,14 +18,14 @@ _LOGGER = logging.getLogger(__name__)
STATE_SMOKE_OFF = 'IDLE_OFF' STATE_SMOKE_OFF = 'IDLE_OFF'
async def async_setup_platform(hass, config, async_add_devices, async def async_setup_platform(
discovery_info=None): hass, config, async_add_devices, discovery_info=None):
"""Set up the binary sensor devices.""" """Set up the HomematicIP Cloud binary sensor devices."""
pass pass
async def async_setup_entry(hass, config_entry, async_add_devices): 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 ( from homematicip.aio.device import (
AsyncShutterContact, AsyncMotionDetectorIndoor, AsyncSmokeDetector) AsyncShutterContact, AsyncMotionDetectorIndoor, AsyncSmokeDetector)
@ -45,7 +44,7 @@ async def async_setup_entry(hass, config_entry, async_add_devices):
class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice): class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice):
"""HomematicIP shutter contact.""" """Representation of a HomematicIP Cloud shutter contact."""
@property @property
def device_class(self): def device_class(self):
@ -65,7 +64,7 @@ class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice):
class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice): class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice):
"""HomematicIP motion detector.""" """Representation of a HomematicIP Cloud motion detector."""
@property @property
def device_class(self): def device_class(self):
@ -81,7 +80,7 @@ class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice):
class HomematicipSmokeDetector(HomematicipGenericDevice, BinarySensorDevice): class HomematicipSmokeDetector(HomematicipGenericDevice, BinarySensorDevice):
"""HomematicIP smoke detector.""" """Representation of a HomematicIP Cloud smoke detector."""
@property @property
def device_class(self): def device_class(self):

View File

@ -2,15 +2,15 @@
Support for INSTEON dimmers via PowerLinc Modem. Support for INSTEON dimmers via PowerLinc Modem.
For more details about this component, please refer to the documentation at 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 asyncio
import logging import logging
from homeassistant.components.binary_sensor import BinarySensorDevice 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__) _LOGGER = logging.getLogger(__name__)
@ -24,27 +24,27 @@ SENSOR_TYPES = {'openClosedSensor': 'opening',
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the INSTEON PLM device class for the hass platform.""" """Set up the INSTEON device class for the hass platform."""
plm = hass.data['insteon_plm'].get('plm') insteon_modem = hass.data['insteon'].get('modem')
address = discovery_info['address'] address = discovery_info['address']
device = plm.devices[address] device = insteon_modem.devices[address]
state_key = discovery_info['state_key'] state_key = discovery_info['state_key']
name = device.states[state_key].name name = device.states[state_key].name
if name != 'dryLeakSensor': if name != 'dryLeakSensor':
_LOGGER.debug('Adding device %s entity %s to Binary Sensor platform', _LOGGER.debug('Adding device %s entity %s to Binary Sensor platform',
device.address.hex, device.states[state_key].name) 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]) async_add_devices([new_entity])
class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice): class InsteonBinarySensor(InsteonEntity, BinarySensorDevice):
"""A Class for an Insteon device entity.""" """A Class for an Insteon device entity."""
def __init__(self, device, state_key): def __init__(self, device, state_key):
"""Initialize the INSTEON PLM binary sensor.""" """Initialize the INSTEON binary sensor."""
super().__init__(device, state_key) super().__init__(device, state_key)
self._sensor_type = SENSOR_TYPES.get(self._insteon_device_state.name) 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): def __init__(self, hass, device):
"""Initialize of KNX binary sensor.""" """Initialize of KNX binary sensor."""
self.device = device self._device = device
self.hass = hass self.hass = hass
self.async_register_callbacks() self.async_register_callbacks()
self.automations = [] self.automations = []
@ -116,12 +116,12 @@ class KNXBinarySensor(BinarySensorDevice):
async def after_update_callback(device): async def after_update_callback(device):
"""Call after device was updated.""" """Call after device was updated."""
await self.async_update_ha_state() 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 @property
def name(self): def name(self):
"""Return the name of the KNX device.""" """Return the name of the KNX device."""
return self.device.name return self._device.name
@property @property
def available(self): def available(self):
@ -136,9 +136,9 @@ class KNXBinarySensor(BinarySensorDevice):
@property @property
def device_class(self): def device_class(self):
"""Return the class of this sensor.""" """Return the class of this sensor."""
return self.device.device_class return self._device.device_class
@property @property
def is_on(self): def is_on(self):
"""Return true if the binary sensor is on.""" """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): def update(self):
"""Retrieve latest state.""" """Retrieve latest state."""
value = getattr(self.device, self.variable) value = getattr(self._device, self.variable)
if self.variable in STRUCTURE_BINARY_TYPES: if self.variable in STRUCTURE_BINARY_TYPES:
self._state = bool(STRUCTURE_BINARY_STATE_MAP self._state = bool(STRUCTURE_BINARY_STATE_MAP
[self.variable].get(value)) [self.variable].get(value))
@ -154,4 +154,5 @@ class NestActivityZoneSensor(NestBinarySensor):
def update(self): def update(self):
"""Retrieve latest state.""" """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 @property
def is_on(self): def is_on(self):
"""Return true if switch is on.""" """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.helpers.event import async_track_state_change
from homeassistant.util import utcnow from homeassistant.util import utcnow
REQUIREMENTS = ['numpy==1.15.0'] REQUIREMENTS = ['numpy==1.15.1']
_LOGGER = logging.getLogger(__name__) _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']: if model in ['motion', 'sensor_motion', 'sensor_motion.aq2']:
devices.append(XiaomiMotionSensor(device, hass, gateway)) devices.append(XiaomiMotionSensor(device, hass, gateway))
elif model in ['magnet', 'sensor_magnet', 'sensor_magnet.aq2']: elif model in ['magnet', 'sensor_magnet', 'sensor_magnet.aq2']:
if 'proto' not in device or int(device['proto'][0:1]) == 1: devices.append(XiaomiDoorSensor(device, gateway))
data_key = 'status'
else:
data_key = 'window_status'
devices.append(XiaomiDoorSensor(device, data_key, gateway))
elif model == 'sensor_wleak.aq1': elif model == 'sensor_wleak.aq1':
devices.append(XiaomiWaterLeakSensor(device, gateway)) devices.append(XiaomiWaterLeakSensor(device, gateway))
elif model in ['smoke', 'sensor_smoke']: 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: if 'proto' not in device or int(device['proto'][0:1]) == 1:
data_key = 'status' data_key = 'status'
else: else:
data_key = 'channel_0' data_key = 'button_0'
devices.append(XiaomiButton(device, 'Switch', data_key, devices.append(XiaomiButton(device, 'Switch', data_key,
hass, gateway)) hass, gateway))
elif model in ['86sw1', 'sensor_86sw1', 'sensor_86sw1.aq1']: 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)) hass, gateway))
elif model in ['86sw2', 'sensor_86sw2', 'sensor_86sw2.aq1']: 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)', devices.append(XiaomiButton(device, 'Wall Switch (Left)',
'channel_0', hass, gateway)) data_key_left, hass, gateway))
devices.append(XiaomiButton(device, 'Wall Switch (Right)', devices.append(XiaomiButton(device, 'Wall Switch (Right)',
'channel_1', hass, gateway)) data_key_right, hass, gateway))
devices.append(XiaomiButton(device, 'Wall Switch (Both)', devices.append(XiaomiButton(device, 'Wall Switch (Both)',
'dual_channel', hass, gateway)) 'dual_channel', hass, gateway))
elif model in ['cube', 'sensor_cube', 'sensor_cube.aqgl01']: elif model in ['cube', 'sensor_cube', 'sensor_cube.aqgl01']:
@ -119,7 +125,7 @@ class XiaomiNatgasSensor(XiaomiBinarySensor):
if value is None: if value is None:
return False return False
if value == '1': if value in ('1', '2'):
if self._state: if self._state:
return False return False
self._state = True self._state = True
@ -194,9 +200,13 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
class XiaomiDoorSensor(XiaomiBinarySensor): class XiaomiDoorSensor(XiaomiBinarySensor):
"""Representation of a XiaomiDoorSensor.""" """Representation of a XiaomiDoorSensor."""
def __init__(self, device, data_key, xiaomi_hub): def __init__(self, device, xiaomi_hub):
"""Initialize the XiaomiDoorSensor.""" """Initialize the XiaomiDoorSensor."""
self._open_since = 0 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', XiaomiBinarySensor.__init__(self, device, 'Door Window Sensor',
xiaomi_hub, data_key, 'opening') xiaomi_hub, data_key, 'opening')
@ -237,8 +247,12 @@ class XiaomiWaterLeakSensor(XiaomiBinarySensor):
def __init__(self, device, xiaomi_hub): def __init__(self, device, xiaomi_hub):
"""Initialize the XiaomiWaterLeakSensor.""" """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', XiaomiBinarySensor.__init__(self, device, 'Water Leak Sensor',
xiaomi_hub, 'status', 'moisture') xiaomi_hub, data_key, 'moisture')
def parse_data(self, data, raw_data): def parse_data(self, data, raw_data):
"""Parse data sent by gateway.""" """Parse data sent by gateway."""
@ -285,7 +299,7 @@ class XiaomiSmokeSensor(XiaomiBinarySensor):
if value is None: if value is None:
return False return False
if value == '1': if value in ('1', '2'):
if self._state: if self._state:
return False return False
self._state = True self._state = True
@ -359,6 +373,10 @@ class XiaomiCube(XiaomiBinarySensor):
self._hass = hass self._hass = hass
self._last_action = None self._last_action = None
self._state = False 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, XiaomiBinarySensor.__init__(self, device, 'Cube', xiaomi_hub,
None, None) None, None)
@ -371,19 +389,12 @@ class XiaomiCube(XiaomiBinarySensor):
def parse_data(self, data, raw_data): def parse_data(self, data, raw_data):
"""Parse data sent by gateway.""" """Parse data sent by gateway."""
if 'status' in data: if self._data_key in data:
self._hass.bus.fire('cube_action', { self._hass.bus.fire('cube_action', {
'entity_id': self.entity_id, 'entity_id': self.entity_id,
'action_type': data['status'] 'action_type': data[self._data_key]
}) })
self._last_action = data['status'] self._last_action = data[self._data_key]
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']
if 'rotate' in data: if 'rotate' in data:
self._hass.bus.fire('cube_action', { self._hass.bus.fire('cube_action', {

View File

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

View File

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

View File

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

View File

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

View File

@ -183,7 +183,7 @@ class ONVIFHassCamera(Camera):
_LOGGER.debug("Camera '%s' doesn't support PTZ.", self._name) _LOGGER.debug("Camera '%s' doesn't support PTZ.", self._name)
async def async_added_to_hass(self): 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: if ONVIF_DATA not in self.hass.data:
self.hass.data[ONVIF_DATA] = {} self.hass.data[ONVIF_DATA] = {}
self.hass.data[ONVIF_DATA][ENTITIES] = [] self.hass.data[ONVIF_DATA][ENTITIES] = []

View File

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

View File

@ -6,7 +6,7 @@
}, },
"step": { "step": {
"confirm": { "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": { "step": {
"confirm": { "confirm": {
"title": "Google Cast", "title": "Google Cast",
"description": "Do you want to setup Google Cast?" "description": "Do you want to set up Google Cast?"
} }
}, },
"abort": { "abort": {

View File

@ -294,7 +294,7 @@ async def async_setup(hass, config):
async def async_setup_entry(hass, entry): 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) return await hass.data[DOMAIN].async_setup_entry(entry)
@ -320,7 +320,7 @@ class ClimateDevice(Entity):
@property @property
def precision(self): def precision(self):
"""Return the precision of the system.""" """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_TENTHS
return PRECISION_WHOLE return PRECISION_WHOLE
@ -394,11 +394,6 @@ class ClimateDevice(Entity):
return data return data
@property
def unit_of_measurement(self):
"""Return the unit of measurement to display."""
return self.hass.config.units.temperature_unit
@property @property
def temperature_unit(self): def temperature_unit(self):
"""Return the unit of measurement used by the platform.""" """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, ATTR_OPERATION_MODE, ATTR_AWAY_MODE, SUPPORT_OPERATION_MODE,
SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA) SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA)
from homeassistant.const import ( from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME, ATTR_ENTITY_ID,
CONF_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_UNKNOWN)
STATE_UNKNOWN)
from homeassistant.helpers import condition from homeassistant.helpers import condition
from homeassistant.helpers.event import ( from homeassistant.helpers.event import (
async_track_state_change, async_track_time_interval) async_track_state_change, async_track_time_interval)
@ -297,11 +296,8 @@ class GenericThermostat(ClimateDevice):
@callback @callback
def _async_update_temp(self, state): def _async_update_temp(self, state):
"""Update thermostat with latest state from sensor.""" """Update thermostat with latest state from sensor."""
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
try: try:
self._cur_temp = self.hass.config.units.temperature( self._cur_temp = float(state.state)
float(state.state), unit)
except ValueError as ex: except ValueError as ex:
_LOGGER.error("Unable to update from sensor: %s", 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): def __init__(self, heatmiser, device, name, serport):
"""Initialize the thermostat.""" """Initialize the thermostat."""
self.heatmiser = heatmiser self.heatmiser = heatmiser
self.device = device
self.serport = serport self.serport = serport
self._current_temperature = None self._current_temperature = None
self._name = name 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/ https://home-assistant.io/components/climate.homematic/
""" """
import logging import logging
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ClimateDevice, STATE_AUTO, SUPPORT_TARGET_TEMPERATURE, STATE_AUTO, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_OPERATION_MODE) ClimateDevice)
from homeassistant.components.homematic import ( from homeassistant.components.homematic import (
HMDevice, ATTR_DISCOVER_DEVICES, HM_ATTRIBUTE_SUPPORT) ATTR_DISCOVER_DEVICES, HM_ATTRIBUTE_SUPPORT, HMDevice)
from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN, ATTR_TEMPERATURE from homeassistant.const import ATTR_TEMPERATURE, STATE_UNKNOWN, TEMP_CELSIUS
DEPENDENCIES = ['homematic'] 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/ https://home-assistant.io/components/climate.homematicip_cloud/
""" """
import logging import logging
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ClimateDevice, SUPPORT_TARGET_TEMPERATURE, ATTR_TEMPERATURE, ATTR_TEMPERATURE, STATE_AUTO, STATE_MANUAL, SUPPORT_TARGET_TEMPERATURE,
STATE_AUTO, STATE_MANUAL) ClimateDevice)
from homeassistant.const import TEMP_CELSIUS
from homeassistant.components.homematicip_cloud import ( from homeassistant.components.homematicip_cloud import (
HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice)
HMIPC_HAPID) from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN
from homeassistant.const import TEMP_CELSIUS
_LOGGER = logging.getLogger(__name__) _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()} 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, async def async_setup_platform(
discovery_info=None): hass, config, async_add_devices, discovery_info=None):
"""Set up the HomematicIP climate devices.""" """Set up the HomematicIP Cloud climate devices."""
pass pass
@ -48,7 +47,7 @@ async def async_setup_entry(hass, config_entry, async_add_devices):
class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice):
"""Representation of a MomematicIP heating group.""" """Representation of a HomematicIP heating group."""
def __init__(self, home, device): def __init__(self, home, device):
"""Initialize heating group.""" """Initialize heating group."""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -212,7 +212,7 @@ class ZWaveProtectionView(HomeAssistantView):
network = hass.data.get(const.DATA_NETWORK) network = hass.data.get(const.DATA_NETWORK)
def _fetch_protection(): def _fetch_protection():
"""Helper to get protection data.""" """Get protection data."""
node = network.nodes.get(nodeid) node = network.nodes.get(nodeid)
if node is None: if node is None:
return self.json_message('Node not found', HTTP_NOT_FOUND) return self.json_message('Node not found', HTTP_NOT_FOUND)
@ -236,7 +236,7 @@ class ZWaveProtectionView(HomeAssistantView):
protection_data = await request.json() protection_data = await request.json()
def _set_protection(): def _set_protection():
"""Helper to get protection data.""" """Set protection data."""
node = network.nodes.get(nodeid) node = network.nodes.get(nodeid)
selection = protection_data["selection"] selection = protection_data["selection"]
value_id = int(protection_data[const.ATTR_VALUE_ID]) 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 import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['brunt==0.1.2'] REQUIREMENTS = ['brunt==0.1.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

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

View File

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

View File

@ -82,7 +82,7 @@ async def async_setup_entry(hass, config_entry):
@callback @callback
def async_add_device_callback(device_type, device): 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( async_dispatcher_send(
hass, 'deconz_new_{}'.format(device_type), [device]) hass, 'deconz_new_{}'.format(device_type), [device])
@ -105,7 +105,7 @@ async def async_setup_entry(hass, config_entry):
@callback @callback
def async_add_remote(sensors): def async_add_remote(sensors):
"""Setup remote from deCONZ.""" """Set up remote from deCONZ."""
from pydeconz.sensor import SWITCH as DECONZ_REMOTE from pydeconz.sensor import SWITCH as DECONZ_REMOTE
allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True) allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True)
for sensor in sensors: for sensor in sensors:

View File

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

View File

@ -15,7 +15,7 @@ from homeassistant.const import (
CONF_HOST, CONF_PORT, CONF_PASSWORD, CONF_USERNAME CONF_HOST, CONF_PORT, CONF_PASSWORD, CONF_USERNAME
) )
REQUIREMENTS = ['ndms2_client==0.0.3'] REQUIREMENTS = ['ndms2_client==0.0.4']
_LOGGER = logging.getLogger(__name__) _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.const import CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers.event import track_utc_time_change from homeassistant.helpers.event import track_utc_time_change
REQUIREMENTS = ['ritassist==0.5'] REQUIREMENTS = ['ritassist==0.9.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -57,7 +57,7 @@ class RitAssistDeviceScanner:
config.get(CONF_PASSWORD)) config.get(CONF_PASSWORD))
def setup(self, hass): def setup(self, hass):
"""Setup a timer and start gathering devices.""" """Set up a timer and start gathering devices."""
self._refresh() self._refresh()
track_utc_time_change(hass, track_utc_time_change(hass,
lambda now: self._refresh(), lambda now: self._refresh(),
@ -78,6 +78,10 @@ class RitAssistDeviceScanner:
for device in devices: for device in devices:
if (not self._include or if (not self._include or
device.license_plate in self._include): 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, self._see(dev_id=device.plate_as_id,
gps=(device.latitude, device.longitude), gps=(device.latitude, device.longitude),
attributes=device.state_attributes, attributes=device.state_attributes,

View File

@ -69,11 +69,16 @@ class TplinkDeviceScanner(DeviceScanner):
password = config[CONF_PASSWORD] password = config[CONF_PASSWORD]
username = config[CONF_USERNAME] username = config[CONF_USERNAME]
self.tplink_client = TpLinkClient( self.success_init = False
password, host=host, username=username) try:
self.tplink_client = TpLinkClient(
password, host=host, username=username)
self.last_results = {} self.last_results = {}
self.success_init = self._update_info()
self.success_init = self._update_info()
except requests.exceptions.ConnectionError:
_LOGGER.debug("ConnectionError in TplinkDeviceScanner")
def scan_devices(self): def scan_devices(self):
"""Scan for new devices and return a list with found device IDs.""" """Scan for new devices and return a list with found device IDs."""
@ -115,7 +120,11 @@ class Tplink1DeviceScanner(DeviceScanner):
self.password = password self.password = password
self.last_results = {} 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): def scan_devices(self):
"""Scan for new devices and return a list with found device IDs.""" """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)), 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): def get_scanner(hass, config):
@ -73,5 +73,8 @@ class XiaomiMiioDeviceScanner(DeviceScanner):
return devices return devices
async def async_get_device_name(self, device): 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 return None

View File

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

View File

@ -139,17 +139,17 @@ class ConfiguredDoorbird():
@property @property
def name(self): def name(self):
"""Custom device name.""" """Get custom device name."""
return self._name return self._name
@property @property
def device(self): def device(self):
"""The configured device.""" """Get the configured device."""
return self._device return self._device
@property @property
def custom_url(self): def custom_url(self):
"""Custom url for device.""" """Get custom url for device."""
return self._custom_url return self._custom_url
@property @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() server.start()
def handle_stop_event(event): def handle_stop_event(event):
"""Callback function for HA stop event.""" """Handle HA stop event."""
server.stop() server.stop()
# listen to home assistant stop event # listen to home assistant stop event

View File

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

View File

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

View File

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

View File

@ -550,12 +550,12 @@ class Group(Entity):
self._async_update_group_state() self._async_update_group_state()
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Callback when added to HASS.""" """Handle addition to HASS."""
if self.tracking: if self.tracking:
self.async_start() self.async_start()
async def async_will_remove_from_hass(self): async def async_will_remove_from_hass(self):
"""Callback when removed from HASS.""" """Handle removal from HASS."""
if self._async_unsub_state_changed: if self._async_unsub_state_changed:
self._async_unsub_state_changed() self._async_unsub_state_changed()
self._async_unsub_state_changed = None 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): async def async_setup(hass, config):
"""Setup the HomeKit component.""" """Set up the HomeKit component."""
_LOGGER.debug('Begin setup HomeKit') _LOGGER.debug('Begin setup HomeKit')
conf = config[DOMAIN] conf = config[DOMAIN]
@ -196,7 +196,7 @@ class HomeKit():
self.driver = None self.driver = None
def setup(self): def setup(self):
"""Setup bridge and accessory driver.""" """Set up bridge and accessory driver."""
from .accessories import HomeBridge, HomeDriver from .accessories import HomeBridge, HomeDriver
self.hass.bus.async_listen_once( self.hass.bus.async_listen_once(

View File

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

View File

@ -109,7 +109,7 @@ def show_setup_message(hass, pincode):
"""Display persistent notification with setup information.""" """Display persistent notification with setup information."""
pin = pincode.decode() pin = pincode.decode()
_LOGGER.info('Pincode: %s', pin) _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) 'following code:\n### {}'.format(pin)
hass.components.persistent_notification.create( hass.components.persistent_notification.create(
message, 'HomeKit Setup', HOMEKIT_NOTIFY_ID) 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.helpers.entity import Entity
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
REQUIREMENTS = ['pyhomematic==0.1.46'] REQUIREMENTS = ['pyhomematic==0.1.47']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -48,6 +48,8 @@ ATTR_MESSAGE = 'message'
ATTR_MODE = 'mode' ATTR_MODE = 'mode'
ATTR_TIME = 'time' ATTR_TIME = 'time'
ATTR_UNIQUE_ID = 'unique_id' ATTR_UNIQUE_ID = 'unique_id'
ATTR_PARAMSET_KEY = 'paramset_key'
ATTR_PARAMSET = 'paramset'
EVENT_KEYPRESS = 'homematic.keypress' EVENT_KEYPRESS = 'homematic.keypress'
EVENT_IMPULSE = 'homematic.impulse' EVENT_IMPULSE = 'homematic.impulse'
@ -58,6 +60,7 @@ SERVICE_RECONNECT = 'reconnect'
SERVICE_SET_VARIABLE_VALUE = 'set_variable_value' SERVICE_SET_VARIABLE_VALUE = 'set_variable_value'
SERVICE_SET_DEVICE_VALUE = 'set_device_value' SERVICE_SET_DEVICE_VALUE = 'set_device_value'
SERVICE_SET_INSTALL_MODE = 'set_install_mode' SERVICE_SET_INSTALL_MODE = 'set_install_mode'
SERVICE_PUT_PARAMSET = 'put_paramset'
HM_DEVICE_TYPES = { HM_DEVICE_TYPES = {
DISCOVER_SWITCHES: [ DISCOVER_SWITCHES: [
@ -78,7 +81,7 @@ HM_DEVICE_TYPES = {
DISCOVER_CLIMATE: [ DISCOVER_CLIMATE: [
'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2', 'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2',
'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall', 'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall',
'ThermostatGroup'], 'ThermostatGroup', 'IPThermostatWall230V'],
DISCOVER_BINARY_SENSORS: [ DISCOVER_BINARY_SENSORS: [
'ShutterContact', 'Smoke', 'SmokeV2', 'Motion', 'MotionV2', 'ShutterContact', 'Smoke', 'SmokeV2', 'Motion', 'MotionV2',
'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor', 'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor',
@ -103,7 +106,7 @@ HM_ATTRIBUTE_SUPPORT = {
'LOW_BAT': ['battery', {0: 'High', 1: 'Low'}], 'LOW_BAT': ['battery', {0: 'High', 1: 'Low'}],
'ERROR': ['sabotage', {0: 'No', 1: 'Yes'}], 'ERROR': ['sabotage', {0: 'No', 1: 'Yes'}],
'SABOTAGE': ['sabotage', {0: 'No', 1: 'Yes'}], 'SABOTAGE': ['sabotage', {0: 'No', 1: 'Yes'}],
'RSSI_DEVICE': ['rssi', {}], 'RSSI_PEER': ['rssi', {}],
'VALVE_STATE': ['valve', {}], 'VALVE_STATE': ['valve', {}],
'BATTERY_STATE': ['battery', {}], 'BATTERY_STATE': ['battery', {}],
'CONTROL_MODE': ['mode', { 'CONTROL_MODE': ['mode', {
@ -232,6 +235,13 @@ SCHEMA_SERVICE_SET_INSTALL_MODE = vol.Schema({
vol.Optional(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), 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 @bind_hass
def virtualkey(hass, address, channel, param, interface=None): 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) 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 @bind_hass
def set_install_mode(hass, interface, mode=None, time=None, address=None): def set_install_mode(hass, interface, mode=None, time=None, address=None):
"""Call setInstallMode XML-RPC method of supplied interface.""" """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, DOMAIN, SERVICE_SET_INSTALL_MODE, _service_handle_install_mode,
schema=SCHEMA_SERVICE_SET_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 return True

View File

@ -66,3 +66,20 @@ set_install_mode:
address: address:
description: (Optional) Address of homematic device or BidCoS-RF to learn description: (Optional) Address of homematic device or BidCoS-RF to learn
example: LEQ3948571 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 For more details about this component, please refer to the documentation at
https://home-assistant.io/components/homematicip_cloud/ https://home-assistant.io/components/homematicip_cloud/
""" """
import logging import logging
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant import config_entries 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 .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 .device import HomematicipGenericDevice # noqa: F401
from .hap import HomematicipAuth, HomematicipHAP # noqa: F401
REQUIREMENTS = ['homematicip==0.9.8'] REQUIREMENTS = ['homematicip==0.9.8']
@ -34,7 +33,7 @@ CONFIG_SCHEMA = vol.Schema({
async def async_setup(hass, config): async def async_setup(hass, config):
"""Set up the HomematicIP component.""" """Set up the HomematicIP Cloud component."""
hass.data[DOMAIN] = {} hass.data[DOMAIN] = {}
accesspoints = config.get(DOMAIN, []) accesspoints = config.get(DOMAIN, [])
@ -54,7 +53,7 @@ async def async_setup(hass, config):
async def async_setup_entry(hass, entry): 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) hap = HomematicipHAP(hass, entry)
hapid = entry.data[HMIPC_HAPID].replace('-', '').upper() hapid = entry.data[HMIPC_HAPID].replace('-', '').upper()
hass.data[DOMAIN][hapid] = hap 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 import voluptuous as vol
from homeassistant import config_entries, data_entry_flow from homeassistant import config_entries, data_entry_flow
from homeassistant.core import callback from homeassistant.core import callback
from .const import ( from .const import DOMAIN as HMIPC_DOMAIN
DOMAIN as HMIPC_DOMAIN, _LOGGER, from .const import HMIPC_AUTHTOKEN, HMIPC_HAPID, HMIPC_NAME, HMIPC_PIN
HMIPC_HAPID, HMIPC_AUTHTOKEN, HMIPC_PIN, HMIPC_NAME) from .const import _LOGGER
from .hap import HomematicipAuth from .hap import HomematicipAuth
@callback @callback
def configured_haps(hass): 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 return set(entry.data[HMIPC_HAPID] for entry
in hass.config_entries.async_entries(HMIPC_DOMAIN)) in hass.config_entries.async_entries(HMIPC_DOMAIN))
@config_entries.HANDLERS.register(HMIPC_DOMAIN) @config_entries.HANDLERS.register(HMIPC_DOMAIN)
class HomematicipCloudFlowHandler(data_entry_flow.FlowHandler): class HomematicipCloudFlowHandler(data_entry_flow.FlowHandler):
"""Config flow HomematicIP Cloud.""" """Config flow for the HomematicIP Cloud component."""
VERSION = 1 VERSION = 1
@ -44,28 +44,28 @@ class HomematicipCloudFlowHandler(data_entry_flow.FlowHandler):
self.auth = HomematicipAuth(self.hass, user_input) self.auth = HomematicipAuth(self.hass, user_input)
connected = await self.auth.async_setup() connected = await self.auth.async_setup()
if connected: if connected:
_LOGGER.info("Connection established") _LOGGER.info("Connection to HomematicIP Cloud established")
return await self.async_step_link() return await self.async_step_link()
return self.async_show_form( return self.async_show_form(
step_id='init', step_id='init',
data_schema=vol.Schema({ data_schema=vol.Schema({
vol.Required(HMIPC_HAPID): str, vol.Required(HMIPC_HAPID): str,
vol.Optional(HMIPC_PIN): str,
vol.Optional(HMIPC_NAME): str, vol.Optional(HMIPC_NAME): str,
vol.Optional(HMIPC_PIN): str,
}), }),
errors=errors errors=errors
) )
async def async_step_link(self, user_input=None): 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 = {} errors = {}
pressed = await self.auth.async_checkbutton() pressed = await self.auth.async_checkbutton()
if pressed: if pressed:
authtoken = await self.auth.async_register() authtoken = await self.auth.async_register()
if authtoken: if authtoken:
_LOGGER.info("Write config entry") _LOGGER.info("Write config entry for HomematicIP Cloud")
return self.async_create_entry( return self.async_create_entry(
title=self.auth.config.get(HMIPC_HAPID), title=self.auth.config.get(HMIPC_HAPID),
data={ data={
@ -73,13 +73,13 @@ class HomematicipCloudFlowHandler(data_entry_flow.FlowHandler):
HMIPC_AUTHTOKEN: authtoken, HMIPC_AUTHTOKEN: authtoken,
HMIPC_NAME: self.auth.config.get(HMIPC_NAME) 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' errors['base'] = 'press_the_button'
return self.async_show_form(step_id='link', errors=errors) return self.async_show_form(step_id='link', errors=errors)
async def async_step_import(self, import_info): 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] hapid = import_info[HMIPC_HAPID]
authtoken = import_info[HMIPC_AUTHTOKEN] authtoken = import_info[HMIPC_AUTHTOKEN]
name = import_info[HMIPC_NAME] name = import_info[HMIPC_NAME]
@ -88,13 +88,13 @@ class HomematicipCloudFlowHandler(data_entry_flow.FlowHandler):
if hapid in configured_haps(self.hass): if hapid in configured_haps(self.hass):
return self.async_abort(reason='already_configured') 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( return self.async_create_entry(
title=hapid, title=hapid,
data={ data={
HMIPC_HAPID: hapid,
HMIPC_AUTHTOKEN: authtoken, HMIPC_AUTHTOKEN: authtoken,
HMIPC_NAME: name HMIPC_HAPID: hapid,
HMIPC_NAME: name,
} }
) )

View File

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

View File

@ -1,25 +1,25 @@
"""GenericDevice for the HomematicIP Cloud component.""" """Generic device for the HomematicIP Cloud component."""
import logging import logging
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_HOME_ID = 'home_id' ATTR_CONNECTED = 'connected'
ATTR_HOME_NAME = 'home_name'
ATTR_DEVICE_ID = 'device_id' ATTR_DEVICE_ID = 'device_id'
ATTR_DEVICE_LABEL = 'device_label' 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_DEVICE_RSSI = 'device_rssi'
ATTR_DUTY_CYCLE = 'duty_cycle' ATTR_DUTY_CYCLE = 'duty_cycle'
ATTR_CONNECTED = 'connected' ATTR_FIRMWARE_STATE = 'firmware_state'
ATTR_SABOTAGE = 'sabotage' 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_OPERATION_LOCK = 'operation_lock'
ATTR_SABOTAGE = 'sabotage'
ATTR_STATUS_UPDATE = 'status_update'
ATTR_UNREACHABLE = 'unreachable'
class HomematicipGenericDevice(Entity): class HomematicipGenericDevice(Entity):
@ -30,8 +30,7 @@ class HomematicipGenericDevice(Entity):
self._home = home self._home = home
self._device = device self._device = device
self.post = post self.post = post
_LOGGER.info('Setting up %s (%s)', self.name, _LOGGER.info("Setting up %s (%s)", self.name, self._device.modelType)
self._device.modelType)
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Register callbacks.""" """Register callbacks."""
@ -39,16 +38,16 @@ class HomematicipGenericDevice(Entity):
def _device_changed(self, json, **kwargs): def _device_changed(self, json, **kwargs):
"""Handle device state changes.""" """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() self.async_schedule_update_ha_state()
@property @property
def name(self): def name(self):
"""Return the name of the generic device.""" """Return the name of the generic device."""
name = self._device.label 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) 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) name = "{} {}".format(name, self.post)
return name return name

View File

@ -1,21 +1,21 @@
"""Errors for the HomematicIP component.""" """Errors for the HomematicIP Cloud component."""
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
class HmipcException(HomeAssistantError): class HmipcException(HomeAssistantError):
"""Base class for HomematicIP exceptions.""" """Base class for HomematicIP Cloud exceptions."""
class HmipcConnectionError(HmipcException): class HmipcConnectionError(HmipcException):
"""Unable to connect to the HomematicIP cloud server.""" """Unable to connect to the HomematicIP Cloud server."""
class HmipcConnectionWait(HmipcException): class HmipcConnectionWait(HmipcException):
"""Wait for registration to the HomematicIP cloud server.""" """Wait for registration to the HomematicIP Cloud server."""
class HmipcRegistrationFailed(HmipcException): class HmipcRegistrationFailed(HmipcException):
"""Registration on HomematicIP cloud failed.""" """Registration on HomematicIP Cloud failed."""
class HmipcPressButton(HmipcException): 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 asyncio
import logging import logging
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import ( from .const import (
HMIPC_HAPID, HMIPC_AUTHTOKEN, HMIPC_PIN, HMIPC_NAME, COMPONENTS, HMIPC_AUTHTOKEN, HMIPC_HAPID, HMIPC_NAME, HMIPC_PIN)
COMPONENTS)
from .errors import HmipcConnectionError from .errors import HmipcConnectionError
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -74,10 +73,10 @@ class HomematicipAuth:
class HomematicipHAP: class HomematicipHAP:
"""Manages HomematicIP http and websocket connection.""" """Manages HomematicIP HTTP and WebSocket connection."""
def __init__(self, hass, config_entry): def __init__(self, hass, config_entry):
"""Initialize HomematicIP cloud connection.""" """Initialize HomematicIP Cloud connection."""
self.hass = hass self.hass = hass
self.config_entry = config_entry self.config_entry = config_entry
self.home = None self.home = None
@ -100,7 +99,7 @@ class HomematicipHAP:
except HmipcConnectionError: except HmipcConnectionError:
retry_delay = 2 ** min(tries + 1, 6) retry_delay = 2 ** min(tries + 1, 6)
_LOGGER.error("Error connecting to HomematicIP with HAP %s. " _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) self.config_entry.data.get(HMIPC_HAPID), retry_delay)
async def retry_setup(_now): async def retry_setup(_now):
@ -113,7 +112,7 @@ class HomematicipHAP:
return False 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)) self.config_entry.data.get(HMIPC_HAPID))
for component in COMPONENTS: for component in COMPONENTS:
@ -127,7 +126,7 @@ class HomematicipHAP:
def async_update(self, *args, **kwargs): def async_update(self, *args, **kwargs):
"""Async update the home device. """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. There are several occasions for this event to happen.
We are only interested to check whether the access point We are only interested to check whether the access point
is still connected. If not, device state changes cannot is still connected. If not, device state changes cannot
@ -147,7 +146,7 @@ class HomematicipHAP:
job.add_done_callback(self.get_state_finished) job.add_done_callback(self.get_state_finished)
async def get_state(self): async def get_state(self):
"""Update hmip state and tell hass.""" """Update HMIP state and tell Home Assistant."""
await self.home.get_current_state() await self.home.get_current_state()
self.update_all() self.update_all()
@ -161,11 +160,11 @@ class HomematicipHAP:
# Somehow connection could not recover. Will disconnect and # Somehow connection could not recover. Will disconnect and
# so reconnect loop is taking over. # so reconnect loop is taking over.
_LOGGER.error( _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()) self.hass.async_add_job(self.home.disable_events())
def set_all_to_unavailable(self): 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: for device in self.home.devices:
device.unreach = True device.unreach = True
self.update_all() self.update_all()
@ -190,7 +189,7 @@ class HomematicipHAP:
return return
async def async_connect(self): async def async_connect(self):
"""Start websocket connection.""" """Start WebSocket connection."""
from homematicip.base.base_connection import HmipConnectionError from homematicip.base.base_connection import HmipConnectionError
tries = 0 tries = 0
@ -210,7 +209,7 @@ class HomematicipHAP:
tries += 1 tries += 1
retry_delay = 2 ** min(tries + 1, 6) retry_delay = 2 ** min(tries + 1, 6)
_LOGGER.error("Error connecting to HomematicIP with HAP %s. " _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) self.config_entry.data.get(HMIPC_HAPID), retry_delay)
try: try:
self._retry_task = self.hass.async_add_job(asyncio.sleep( self._retry_task = self.hass.async_add_job(asyncio.sleep(
@ -227,7 +226,7 @@ class HomematicipHAP:
if self._retry_task is not None: if self._retry_task is not None:
self._retry_task.cancel() self._retry_task.cancel()
self.home.disable_events() self.home.disable_events()
_LOGGER.info("Closed connection to HomematicIP cloud server.") _LOGGER.info("Closed connection to HomematicIP cloud server")
for component in COMPONENTS: for component in COMPONENTS:
await self.hass.config_entries.async_forward_entry_unload( await self.hass.config_entries.async_forward_entry_unload(
self.config_entry, component) self.config_entry, component)

View File

@ -3,16 +3,16 @@
"title": "HomematicIP Cloud", "title": "HomematicIP Cloud",
"step": { "step": {
"init": { "init": {
"title": "Pick HomematicIP Accesspoint", "title": "Pick HomematicIP Access point",
"data": { "data": {
"hapid": "Accesspoint ID (SGTIN)", "hapid": "Access point ID (SGTIN)",
"pin": "Pin Code (optional)", "pin": "Pin Code (optional)",
"name": "Name (optional, used as name prefix for all devices)" "name": "Name (optional, used as name prefix for all devices)"
} }
}, },
"link": { "link": {
"title": "Link Accesspoint", "title": "Link Access point",
"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)" "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": { "error": {
@ -23,8 +23,8 @@
}, },
"abort": { "abort": {
"unknown": "Unknown error occurred.", "unknown": "Unknown error occurred.",
"conection_aborted": "Could not connect to HMIP server", "connection_aborted": "Could not connect to HMIP server",
"already_configured": "Accesspoint is already configured" "already_configured": "Access point is already configured"
} }
} }
} }

View File

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

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