mirror of
https://github.com/home-assistant/core.git
synced 2026-01-16 04:26:54 +01:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4063b24ddb | ||
|
|
46734b8409 | ||
|
|
b9c80a6bb3 | ||
|
|
669b3458b9 | ||
|
|
976626d0ab | ||
|
|
d705375a9a | ||
|
|
bc618af193 | ||
|
|
0e076fb9e7 | ||
|
|
16a58bd1cf | ||
|
|
8be7a0a9b9 | ||
|
|
232076b41d | ||
|
|
e77344f029 | ||
|
|
60438067f8 | ||
|
|
9062de0704 | ||
|
|
64453638bb | ||
|
|
5f1282a4ab | ||
|
|
9db15aab92 | ||
|
|
f01e1ef0aa | ||
|
|
b5919ce92c | ||
|
|
f9b1fb5906 | ||
|
|
9238261e17 | ||
|
|
8ec109d255 | ||
|
|
2ea2bcab77 | ||
|
|
8d38016b0c | ||
|
|
d994d6bfad | ||
|
|
573f5de148 | ||
|
|
667f9c6fe4 | ||
|
|
f708292015 | ||
|
|
c50a7deb92 | ||
|
|
11fcffda4c | ||
|
|
e9cc359abe | ||
|
|
9b01972b41 | ||
|
|
3e65009ea9 | ||
|
|
a953601abd | ||
|
|
2744702f9b | ||
|
|
9c7d4381a1 | ||
|
|
914436f3d5 | ||
|
|
adb5579690 |
@@ -52,6 +52,8 @@ homeassistant/components/cover/template.py @PhracturedBlue
|
||||
homeassistant/components/device_tracker/automatic.py @armills
|
||||
homeassistant/components/device_tracker/tile.py @bachya
|
||||
homeassistant/components/history_graph.py @andrey-git
|
||||
homeassistant/components/light/lifx.py @amelchio
|
||||
homeassistant/components/light/lifx_legacy.py @amelchio
|
||||
homeassistant/components/light/tplink.py @rytilahti
|
||||
homeassistant/components/light/yeelight.py @rytilahti
|
||||
homeassistant/components/lock/nello.py @pschmitt
|
||||
@@ -65,6 +67,7 @@ homeassistant/components/media_player/sonos.py @amelchio
|
||||
homeassistant/components/media_player/xiaomi_tv.py @fattdev
|
||||
homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth
|
||||
homeassistant/components/plant.py @ChristianKuehnel
|
||||
homeassistant/components/scene/lifx_cloud.py @amelchio
|
||||
homeassistant/components/sensor/airvisual.py @bachya
|
||||
homeassistant/components/sensor/filter.py @dgomes
|
||||
homeassistant/components/sensor/gearbest.py @HerrHofrat
|
||||
|
||||
@@ -24,7 +24,11 @@ async def auth_manager_from_config(
|
||||
hass: HomeAssistant,
|
||||
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.
|
||||
|
||||
CORE_CONFIG_SCHEMA will make sure do duplicated auth providers or
|
||||
mfa modules exist in configs.
|
||||
"""
|
||||
store = auth_store.AuthStore(hass)
|
||||
if provider_configs:
|
||||
providers = await asyncio.gather(
|
||||
@@ -35,17 +39,7 @@ async def auth_manager_from_config(
|
||||
# So returned auth providers are in same order as config
|
||||
provider_hash = OrderedDict() # type: _ProviderDict
|
||||
for provider in providers:
|
||||
if provider is None:
|
||||
continue
|
||||
|
||||
key = (provider.type, provider.id)
|
||||
|
||||
if key in provider_hash:
|
||||
_LOGGER.error(
|
||||
'Found duplicate provider: %s. Please add unique IDs if you '
|
||||
'want to have the same provider twice.', key)
|
||||
continue
|
||||
|
||||
provider_hash[key] = provider
|
||||
|
||||
if module_configs:
|
||||
@@ -57,15 +51,6 @@ async def auth_manager_from_config(
|
||||
# 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)
|
||||
|
||||
@@ -11,6 +11,7 @@ from voluptuous.humanize import humanize_error
|
||||
from homeassistant import requirements, data_entry_flow
|
||||
from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.util.decorator import Registry
|
||||
|
||||
MULTI_FACTOR_AUTH_MODULES = Registry()
|
||||
@@ -127,34 +128,32 @@ class SetupFlow(data_entry_flow.FlowHandler):
|
||||
|
||||
async def auth_mfa_module_from_config(
|
||||
hass: HomeAssistant, config: Dict[str, Any]) \
|
||||
-> Optional[MultiFactorAuthModule]:
|
||||
-> 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
|
||||
raise
|
||||
|
||||
return MULTI_FACTOR_AUTH_MODULES[module_name](hass, config) # type: ignore
|
||||
|
||||
|
||||
async def _load_mfa_module(hass: HomeAssistant, module_name: str) \
|
||||
-> Optional[types.ModuleType]:
|
||||
-> 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
|
||||
except ImportError as err:
|
||||
_LOGGER.error('Unable to load mfa module %s: %s', module_name, err)
|
||||
raise HomeAssistantError('Unable to load mfa module {}: {}'.format(
|
||||
module_name, err))
|
||||
|
||||
if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'):
|
||||
return module
|
||||
@@ -170,7 +169,9 @@ async def _load_mfa_module(hass: HomeAssistant, module_name: str) \
|
||||
hass, module_path, module.REQUIREMENTS) # type: ignore
|
||||
|
||||
if not req_success:
|
||||
return None
|
||||
raise HomeAssistantError(
|
||||
'Unable to process requirements of mfa module {}'.format(
|
||||
module_name))
|
||||
|
||||
processed.add(module_name)
|
||||
return module
|
||||
|
||||
@@ -137,8 +137,9 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||
await self._async_load()
|
||||
|
||||
# user_input has been validate in caller
|
||||
# set INPUT_FIELD_CODE as vol.Required is not user friendly
|
||||
return await self.hass.async_add_executor_job(
|
||||
self._validate_2fa, user_id, user_input[INPUT_FIELD_CODE])
|
||||
self._validate_2fa, user_id, user_input.get(INPUT_FIELD_CODE, ''))
|
||||
|
||||
def _validate_2fa(self, user_id: str, code: str) -> bool:
|
||||
"""Validate two factor authentication code."""
|
||||
|
||||
@@ -10,6 +10,7 @@ from voluptuous.humanize import humanize_error
|
||||
from homeassistant import data_entry_flow, requirements
|
||||
from homeassistant.core import callback, HomeAssistant
|
||||
from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.decorator import Registry
|
||||
|
||||
@@ -110,33 +111,31 @@ class AuthProvider:
|
||||
|
||||
async def auth_provider_from_config(
|
||||
hass: HomeAssistant, store: AuthStore,
|
||||
config: Dict[str, Any]) -> Optional[AuthProvider]:
|
||||
config: Dict[str, Any]) -> AuthProvider:
|
||||
"""Initialize an auth provider from a config."""
|
||||
provider_name = config[CONF_TYPE]
|
||||
module = await load_auth_provider_module(hass, provider_name)
|
||||
|
||||
if module is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
config = module.CONFIG_SCHEMA(config) # type: ignore
|
||||
except vol.Invalid as err:
|
||||
_LOGGER.error('Invalid configuration for auth provider %s: %s',
|
||||
provider_name, humanize_error(config, err))
|
||||
return None
|
||||
raise
|
||||
|
||||
return AUTH_PROVIDERS[provider_name](hass, store, config) # type: ignore
|
||||
|
||||
|
||||
async def load_auth_provider_module(
|
||||
hass: HomeAssistant, provider: str) -> Optional[types.ModuleType]:
|
||||
hass: HomeAssistant, provider: str) -> types.ModuleType:
|
||||
"""Load an auth provider."""
|
||||
try:
|
||||
module = importlib.import_module(
|
||||
'homeassistant.auth.providers.{}'.format(provider))
|
||||
except ImportError:
|
||||
_LOGGER.warning('Unable to find auth provider %s', provider)
|
||||
return None
|
||||
except ImportError as err:
|
||||
_LOGGER.error('Unable to load auth provider %s: %s', provider, err)
|
||||
raise HomeAssistantError('Unable to load auth provider {}: {}'.format(
|
||||
provider, err))
|
||||
|
||||
if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'):
|
||||
return module
|
||||
@@ -154,7 +153,9 @@ async def load_auth_provider_module(
|
||||
hass, 'auth provider {}'.format(provider), reqs)
|
||||
|
||||
if not req_success:
|
||||
return None
|
||||
raise HomeAssistantError(
|
||||
'Unable to process requirements of auth provider {}'.format(
|
||||
provider))
|
||||
|
||||
processed.add(provider)
|
||||
return module
|
||||
@@ -223,19 +224,27 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||
if user_input is not None:
|
||||
expires = self.created_at + SESSION_EXPIRATION
|
||||
if dt_util.utcnow() > expires:
|
||||
errors['base'] = 'login_expired'
|
||||
else:
|
||||
result = await auth_module.async_validation(
|
||||
self.user.id, user_input) # type: ignore
|
||||
if not result:
|
||||
errors['base'] = 'invalid_auth'
|
||||
return self.async_abort(
|
||||
reason='login_expired'
|
||||
)
|
||||
|
||||
result = await auth_module.async_validation(
|
||||
self.user.id, user_input) # type: ignore
|
||||
if not result:
|
||||
errors['base'] = 'invalid_code'
|
||||
|
||||
if not errors:
|
||||
return await self.async_finish(self.user)
|
||||
|
||||
description_placeholders = {
|
||||
'mfa_module_name': auth_module.name,
|
||||
'mfa_module_id': auth_module.id
|
||||
} # type: Dict[str, str]
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='mfa',
|
||||
data_schema=auth_module.input_schema,
|
||||
description_placeholders=description_placeholders,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
|
||||
@@ -111,31 +111,19 @@ class TrustedNetworksLoginFlow(LoginFlow):
|
||||
self, user_input: Optional[Dict[str, str]] = None) \
|
||||
-> Dict[str, Any]:
|
||||
"""Handle the step of the form."""
|
||||
errors = {}
|
||||
try:
|
||||
cast(TrustedNetworksAuthProvider, self._auth_provider)\
|
||||
.async_validate_access(self._ip_address)
|
||||
|
||||
except InvalidAuthError:
|
||||
errors['base'] = 'invalid_auth'
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=None,
|
||||
errors=errors,
|
||||
return self.async_abort(
|
||||
reason='not_whitelisted'
|
||||
)
|
||||
|
||||
if user_input is not None:
|
||||
user_id = user_input['user']
|
||||
if user_id not in self._available_users:
|
||||
errors['base'] = 'invalid_auth'
|
||||
|
||||
if not errors:
|
||||
return await self.async_finish(user_input)
|
||||
|
||||
schema = {'user': vol.In(self._available_users)}
|
||||
return await self.async_finish(user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=vol.Schema(schema),
|
||||
errors=errors,
|
||||
data_schema=vol.Schema({'user': vol.In(self._available_users)}),
|
||||
)
|
||||
|
||||
@@ -61,7 +61,6 @@ def from_config_dict(config: Dict[str, Any],
|
||||
config, hass, config_dir, enable_log, verbose, skip_pip,
|
||||
log_rotate_days, log_file, log_no_color)
|
||||
)
|
||||
|
||||
return hass
|
||||
|
||||
|
||||
@@ -94,8 +93,13 @@ async def async_from_config_dict(config: Dict[str, Any],
|
||||
try:
|
||||
await conf_util.async_process_ha_core_config(
|
||||
hass, core_config, has_api_password, has_trusted_networks)
|
||||
except vol.Invalid as ex:
|
||||
conf_util.async_log_exception(ex, 'homeassistant', core_config, hass)
|
||||
except vol.Invalid as config_err:
|
||||
conf_util.async_log_exception(
|
||||
config_err, 'homeassistant', core_config, hass)
|
||||
return None
|
||||
except HomeAssistantError:
|
||||
_LOGGER.error("Home Assistant core failed to initialize. "
|
||||
"Further initialization aborted")
|
||||
return None
|
||||
|
||||
await hass.async_add_executor_job(
|
||||
@@ -130,7 +134,7 @@ async def async_from_config_dict(config: Dict[str, Any],
|
||||
res = await core_components.async_setup(hass, config)
|
||||
if not res:
|
||||
_LOGGER.error("Home Assistant core failed to initialize. "
|
||||
"further initialization aborted")
|
||||
"Further initialization aborted")
|
||||
return hass
|
||||
|
||||
await persistent_notification.async_setup(hass, config)
|
||||
|
||||
7
homeassistant/components/auth/.translations/ar.json
Normal file
7
homeassistant/components/auth/.translations/ar.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"totp": {
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
homeassistant/components/auth/.translations/ca.json
Normal file
16
homeassistant/components/auth/.translations/ca.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "Codi no v\u00e0lid, si us plau torni a provar-ho. Si obteniu aquest error repetidament, assegureu-vos que la data i hora de Home Assistant sigui correcta i precisa."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Per activar la verificaci\u00f3 en dos passos mitjan\u00e7ant contrasenyes d'un sol \u00fas basades en temps, escanegeu el codi QR amb la vostre aplicaci\u00f3 de verificaci\u00f3. Si no en teniu cap, us recomanem [Google Authenticator](https://support.google.com/accounts/answer/1066447) o b\u00e9 [Authy](https://authy.com/). \n\n {qr_code} \n \nDespr\u00e9s d'escanejar el codi QR, introdu\u00efu el codi de sis d\u00edgits proporcionat per l'aplicaci\u00f3. Si teniu problemes per escanejar el codi QR, feu una configuraci\u00f3 manual amb el codi **`{code}`**.",
|
||||
"title": "Configureu la verificaci\u00f3 en dos passos utilitzant TOTP"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
homeassistant/components/auth/.translations/de.json
Normal file
16
homeassistant/components/auth/.translations/de.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "Ung\u00fcltiger Code, bitte versuche es erneut. Wenn Sie diesen Fehler regelm\u00e4\u00dfig erhalten, stelle sicher, dass die Uhr deines Home Assistant-Systems korrekt ist."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Um die Zwei-Faktor-Authentifizierung mit zeitbasierten Einmalpassw\u00f6rtern zu aktivieren, scanne den QR-Code mit Ihrer Authentifizierungs-App. Wenn du keine hast, empfehlen wir entweder [Google Authenticator] (https://support.google.com/accounts/answer/1066447) oder [Authy] (https://authy.com/). \n\n {qr_code} \n \nNachdem du den Code gescannt hast, gebe den sechsstelligen Code aus der App ein, um das Setup zu \u00fcberpr\u00fcfen. Wenn es Probleme beim Scannen des QR-Codes gibt, f\u00fchre ein manuelles Setup mit dem Code ** ` {code} ` ** durch.",
|
||||
"title": "Richte die Zwei-Faktor-Authentifizierung mit TOTP ein"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,12 @@
|
||||
"mfa_setup": {
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "Invalid code, please try again. If you get this error consistently, please make sure the clock on Home Assistant system is accurate."
|
||||
"invalid_code": "Invalid code, please try again. If you get this error consistently, please make sure the clock of your Home Assistant system is accurate."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Scan the QR code with your authentication app, such as **Google Authenticator** or **Authy**. If you have problem to scan the QR code, using **`{code}`** to manual setup. \n\n{qr_code}\n\nEnter the six digi code appeared in your app below to verify the setup:",
|
||||
"title": "Scan this QR code with your app"
|
||||
"description": "To activate two factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator](https://support.google.com/accounts/answer/1066447) or [Authy](https://authy.com/).\n\n{qr_code}\n\nAfter scanning the code, enter the six digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**.",
|
||||
"title": "Set up two-factor authentication using TOTP"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
|
||||
12
homeassistant/components/auth/.translations/es-419.json
Normal file
12
homeassistant/components/auth/.translations/es-419.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"totp": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Configurar la autenticaci\u00f3n de dos factores mediante TOTP"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
homeassistant/components/auth/.translations/fr.json
Normal file
16
homeassistant/components/auth/.translations/fr.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "Code invalide. S'il vous pla\u00eet essayez \u00e0 nouveau. Si cette erreur persiste, assurez-vous que l'horloge de votre syst\u00e8me Home Assistant est correcte."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Pour activer l'authentification \u00e0 deux facteurs \u00e0 l'aide de mots de passe \u00e0 utilisation unique bas\u00e9s sur l'heure, num\u00e9risez le code QR avec votre application d'authentification. Si vous n'en avez pas, nous vous recommandons d'utiliser [Google Authenticator] (https://support.google.com/accounts/answer/1066447) ou [Authy] (https://authy.com/). \n\n {qr_code} \n \n Apr\u00e8s avoir num\u00e9ris\u00e9 le code, entrez le code \u00e0 six chiffres de votre application pour v\u00e9rifier la configuration. Si vous rencontrez des probl\u00e8mes lors de l\u2019analyse du code QR, effectuez une configuration manuelle avec le code ** ` {code} ` **.",
|
||||
"title": "Configurer une authentification \u00e0 deux facteurs \u00e0 l'aide de TOTP"
|
||||
}
|
||||
},
|
||||
"title": "TOTP (Mot de passe \u00e0 utilisation unique bas\u00e9 sur le temps)"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
homeassistant/components/auth/.translations/hu.json
Normal file
16
homeassistant/components/auth/.translations/hu.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "\u00c9rv\u00e9nytelen k\u00f3d, pr\u00f3b\u00e1ld \u00fajra. Ha ez a hiba folyamatosan el\u0151fordul, akkor gy\u0151z\u0151dj meg r\u00f3la, hogy a Home Assistant rendszered \u00f3r\u00e1ja pontosan j\u00e1r."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Ahhoz, hogy haszn\u00e1lhasd a k\u00e9tfaktoros hiteles\u00edt\u00e9st id\u0151alap\u00fa egyszeri jelszavakkal, szkenneld be a QR k\u00f3dot a hiteles\u00edt\u00e9si applik\u00e1ci\u00f3ddal. Ha m\u00e9g nincsen, akkor a [Google Hiteles\u00edt\u0151](https://support.google.com/accounts/answer/1066447)t vagy az [Authy](https://authy.com/)-t aj\u00e1nljuk.\n\n{qr_code}\n\nA k\u00f3d beolvas\u00e1sa ut\u00e1n add meg a hat sz\u00e1mjegy\u0171 k\u00f3dot az applik\u00e1ci\u00f3b\u00f3l a telep\u00edt\u00e9s ellen\u0151rz\u00e9s\u00e9hez. Ha probl\u00e9m\u00e1ba \u00fctk\u00f6z\u00f6l a QR k\u00f3d beolvas\u00e1s\u00e1n\u00e1l, akkor ind\u00edts egy k\u00e9zi be\u00e1ll\u00edt\u00e1st a **`{code}`** k\u00f3ddal.",
|
||||
"title": "K\u00e9tfaktoros hiteles\u00edt\u00e9s be\u00e1ll\u00edt\u00e1sa TOTP haszn\u00e1lat\u00e1val"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
13
homeassistant/components/auth/.translations/it.json
Normal file
13
homeassistant/components/auth/.translations/it.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"totp": {
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Per attivare l'autenticazione a due fattori utilizzando password monouso basate sul tempo, eseguire la scansione del codice QR con l'app di autenticazione. Se non ne hai uno, ti consigliamo [Google Authenticator] (https://support.google.com/accounts/answer/1066447) o [Authy] (https://authy.com/). \n\n {qr_code} \n \n Dopo aver scansionato il codice, inserisci il codice a sei cifre dalla tua app per verificare la configurazione. Se riscontri problemi con la scansione del codice QR, esegui una configurazione manuale con codice ** ` {code} ` **.",
|
||||
"title": "Imposta l'autenticazione a due fattori usando TOTP"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
homeassistant/components/auth/.translations/ko.json
Normal file
16
homeassistant/components/auth/.translations/ko.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "\uc798\ubabb\ub41c \ucf54\ub4dc \uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694. \uc774 \uc624\ub958\uac00 \uc9c0\uc18d\uc801\uc73c\ub85c \ubc1c\uc0dd\ud55c\ub2e4\uba74 Home Assistant \uc758 \uc2dc\uac04\uc124\uc815\uc774 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud574\ubcf4\uc138\uc694."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574 \uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.",
|
||||
"title": "TOTP \ub97c \uc0ac\uc6a9\ud558\uc5ec 2 \ub2e8\uacc4 \uc778\uc99d \uad6c\uc131"
|
||||
}
|
||||
},
|
||||
"title": "TOTP (\uc2dc\uac04 \uae30\ubc18 OTP)"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
homeassistant/components/auth/.translations/lb.json
Normal file
16
homeassistant/components/auth/.translations/lb.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "Ong\u00ebltege Login, prob\u00e9iert w.e.g. nach emol. Falls d\u00ebse Feeler Message \u00ebmmer er\u00ebm optr\u00ebtt dann iwwerpr\u00e9ift op d'Z\u00e4it vum Home Assistant System richteg ass."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Fir d'Zwee-Faktor-Authentifikatioun m\u00ebttels engem Z\u00e4it bas\u00e9ierten eemolege Passwuert z'aktiv\u00e9ieren, scannt de QR Code mat enger Authentifikatioun's App.\nFalls dir keng hutt, recommand\u00e9iere mir entweder [Google Authenticator](https://support.google.com/accounts/answer/1066447) oder [Authy](https://authy.com/).\n\n{qr_code}\n\nNodeems de Code gescannt ass, gitt de sechs stellege Code vun der App a fir d'Konfiguratioun z'iwwerpr\u00e9iwen. Am Fall vu Problemer fir de QR Code ze scannen, gitt de folgende Code **`{code}`** a fir ee manuelle Setup.",
|
||||
"title": "Zwee Faktor Authentifikatioun mat TOTP konfigur\u00e9ieren"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
homeassistant/components/auth/.translations/nl.json
Normal file
16
homeassistant/components/auth/.translations/nl.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "Ongeldige code, probeer het opnieuw. Als u deze fout blijft krijgen, controleer dan of de klok van uw Home Assistant systeem correct is ingesteld."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Voor het activeren van twee-factor-authenticatie via tijdgebonden eenmalige wachtwoorden: scan de QR code met uw authenticatie-app. Als u nog geen app heeft, adviseren we [Google Authenticator (https://support.google.com/accounts/answer/1066447) of [Authy](https://authy.com/).\n\n{qr_code}\n\nNa het scannen van de code voert u de zescijferige code uit uw app in om de instelling te controleren. Als u problemen heeft met het scannen van de QR-code, voert u een handmatige configuratie uit met code **`{code}`**.",
|
||||
"title": "Configureer twee-factor-authenticatie via TOTP"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
homeassistant/components/auth/.translations/no.json
Normal file
16
homeassistant/components/auth/.translations/no.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "Ugyldig kode, pr\u00f8v igjen. Hvis du f\u00e5r denne feilen konsekvent, m\u00e5 du s\u00f8rge for at klokken p\u00e5 Home Assistant systemet er riktig."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "For \u00e5 aktivere tofaktorautentisering ved hjelp av tidsbaserte engangspassord, skann QR-koden med autentiseringsappen din. Hvis du ikke har en, kan vi anbefale enten [Google Authenticator](https://support.google.com/accounts/answer/1066447) eller [Authy](https://authy.com/). \n\n {qr_code} \n \nEtter at du har skannet koden, skriver du inn den seks-sifrede koden fra appen din for \u00e5 kontrollere oppsettet. Dersom du har problemer med \u00e5 skanne QR-koden kan du taste inn f\u00f8lgende kode manuelt: **`{code}`**.",
|
||||
"title": "Konfigurer tofaktorautentisering ved hjelp av TOTP"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
homeassistant/components/auth/.translations/pl.json
Normal file
16
homeassistant/components/auth/.translations/pl.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "Nieprawid\u0142owy kod, spr\u00f3buj ponownie. Je\u015bli b\u0142\u0105d b\u0119dzie si\u0119 powtarza\u0142, upewnij si\u0119, \u017ce czas zegara systemu Home Assistant jest prawid\u0142owy."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Aby aktywowa\u0107 uwierzytelnianie dwusk\u0142adnikowe przy u\u017cyciu jednorazowych hase\u0142 opartych na czasie, zeskanuj kod QR za pomoc\u0105 aplikacji uwierzytelniaj\u0105cej. Je\u015bli jej nie masz, polecamy [Google Authenticator](https://support.google.com/accounts/answer/1066447) lub [Authy](https://authy.com/).\n\n{qr_code} \n \nPo zeskanowaniu kodu wprowad\u017a sze\u015bciocyfrowy kod z aplikacji, aby zweryfikowa\u0107 konfiguracj\u0119. Je\u015bli masz problemy z zeskanowaniem kodu QR, wykonaj r\u0119czn\u0105 konfiguracj\u0119 z kodem **`{code}`**.",
|
||||
"title": "Skonfiguruj uwierzytelnianie dwusk\u0142adnikowe za pomoc\u0105 hase\u0142 jednorazowych opartych na czasie"
|
||||
}
|
||||
},
|
||||
"title": "Has\u0142a jednorazowe oparte na czasie"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
homeassistant/components/auth/.translations/pt.json
Normal file
16
homeassistant/components/auth/.translations/pt.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "C\u00f3digo inv\u00e1lido, por favor, tente novamente. Se receber este erro constantemente, por favor, certifique-se de que o rel\u00f3gio do sistema que hospeda o Home Assistent \u00e9 preciso."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Para ativar a autentica\u00e7\u00e3o com dois fatores utilizando passwords unicas temporais (OTP), ler o c\u00f3digo QR com a sua aplica\u00e7\u00e3o de autentica\u00e7\u00e3o. Se voc\u00ea n\u00e3o tiver uma, recomendamos [Google Authenticator](https://support.google.com/accounts/answer/1066447) ou [Authy](https://authy.com/).\n\n{qr_code}\n\nDepois de ler o c\u00f3digo, introduza o c\u00f3digo de seis d\u00edgitos fornecido pela sua aplica\u00e7\u00e3o para verificar a configura\u00e7\u00e3o. Se tiver problemas a ler o c\u00f3digo QR, fa\u00e7a uma configura\u00e7\u00e3o manual com o c\u00f3digo **`{c\u00f3digo}`**.",
|
||||
"title": "Configurar autentica\u00e7\u00e3o com dois fatores usando TOTP"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
homeassistant/components/auth/.translations/ru.json
Normal file
16
homeassistant/components/auth/.translations/ru.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430. \u0415\u0441\u043b\u0438 \u0432\u044b \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442\u0435 \u044d\u0442\u0443 \u043e\u0448\u0438\u0431\u043a\u0443, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0447\u0430\u0441\u044b \u0432 \u0432\u0430\u0448\u0435\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Home Assistant \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u044e\u0442 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0435 \u0432\u0440\u0435\u043c\u044f."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "\u0427\u0442\u043e\u0431\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0443\u044e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0445 \u043f\u0430\u0440\u043e\u043b\u0435\u0439, \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \u043d\u0430 \u0432\u0440\u0435\u043c\u0435\u043d\u0438, \u043e\u0442\u0441\u043a\u0430\u043d\u0438\u0440\u0443\u0439\u0442\u0435 QR-\u043a\u043e\u0434 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043f\u043e\u0434\u043b\u0438\u043d\u043d\u043e\u0441\u0442\u0438. \u0415\u0441\u043b\u0438 \u0443 \u0432\u0430\u0441 \u0435\u0433\u043e \u043d\u0435\u0442, \u043c\u044b \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043b\u0438\u0431\u043e [Google Authenticator](https://support.google.com/accounts/answer/1066447), \u043b\u0438\u0431\u043e [Authy](https://authy.com/). \n\n {qr_code} \n \n\u041f\u043e\u0441\u043b\u0435 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f QR-\u043a\u043e\u0434\u0430 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0448\u0435\u0441\u0442\u0438\u0437\u043d\u0430\u0447\u043d\u044b\u0439 \u043a\u043e\u0434 \u0438\u0437 \u0432\u0430\u0448\u0435\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u0415\u0441\u043b\u0438 \u0443 \u0432\u0430\u0441 \u0435\u0441\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441\u043e \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\u043c QR-\u043a\u043e\u0434\u0430, \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043a\u043e\u0434\u0430 **`{code}`**.",
|
||||
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c TOTP"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
homeassistant/components/auth/.translations/sl.json
Normal file
16
homeassistant/components/auth/.translations/sl.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "Neveljavna koda, prosimo, poskusite znova. \u010ce dobite to sporo\u010dilo ve\u010dkrat, prosimo poskrbite, da bo ura va\u0161ega Home Assistenta to\u010dna."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "\u010ce \u017eelite aktivirati preverjanje pristnosti dveh faktorjev z enkratnimi gesli, ki temeljijo na \u010dasu, skenirajte kodo QR s svojo aplikacijo za preverjanje pristnosti. \u010ce je nimate, priporo\u010damo bodisi [Google Authenticator] (https://support.google.com/accounts/answer/1066447) ali [Authy] (https://authy.com/). \n\n {qr_code} \n \n Po skeniranju kode vnesite \u0161estmestno kodo iz aplikacije, da preverite nastavitev. \u010ce imate te\u017eave pri skeniranju kode QR, naredite ro\u010dno nastavitev s kodo ** ` {code} ` **.",
|
||||
"title": "Nastavite dvofaktorsko avtentifikacijo s pomo\u010djo TOTP-ja"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
homeassistant/components/auth/.translations/zh-Hans.json
Normal file
16
homeassistant/components/auth/.translations/zh-Hans.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "\u53e3\u4ee4\u65e0\u6548\uff0c\u8bf7\u91cd\u65b0\u8f93\u5165\u3002\u5982\u679c\u9519\u8bef\u53cd\u590d\u51fa\u73b0\uff0c\u8bf7\u786e\u4fdd Home Assistant \u7cfb\u7edf\u7684\u65f6\u95f4\u51c6\u786e\u65e0\u8bef\u3002"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "\u8981\u6fc0\u6d3b\u57fa\u4e8e\u65f6\u95f4\u52a8\u6001\u53e3\u4ee4\u7684\u53cc\u91cd\u8ba4\u8bc1\uff0c\u8bf7\u7528\u8eab\u4efd\u9a8c\u8bc1\u5e94\u7528\u626b\u63cf\u4ee5\u4e0b\u4e8c\u7ef4\u7801\u3002\u5982\u679c\u60a8\u8fd8\u6ca1\u6709\u8eab\u4efd\u9a8c\u8bc1\u5e94\u7528\uff0c\u63a8\u8350\u4f7f\u7528 [Google \u8eab\u4efd\u9a8c\u8bc1\u5668](https://support.google.com/accounts/answer/1066447) \u6216 [Authy](https://authy.com/)\u3002\n\n{qr_code}\n\n\u626b\u63cf\u4e8c\u7ef4\u7801\u4ee5\u540e\uff0c\u8f93\u5165\u5e94\u7528\u4e0a\u7684\u516d\u4f4d\u6570\u5b57\u53e3\u4ee4\u6765\u9a8c\u8bc1\u914d\u7f6e\u3002\u5982\u679c\u5728\u626b\u63cf\u4e8c\u7ef4\u7801\u65f6\u9047\u5230\u95ee\u9898\uff0c\u8bf7\u4f7f\u7528\u4ee3\u7801 **`{code}`** \u624b\u52a8\u914d\u7f6e\u3002",
|
||||
"title": "\u7528\u65f6\u95f4\u52a8\u6001\u53e3\u4ee4\u8bbe\u7f6e\u53cc\u91cd\u8ba4\u8bc1"
|
||||
}
|
||||
},
|
||||
"title": "\u65f6\u95f4\u52a8\u6001\u53e3\u4ee4"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
homeassistant/components/auth/.translations/zh-Hant.json
Normal file
16
homeassistant/components/auth/.translations/zh-Hant.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "\u9a57\u8b49\u78bc\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002\u5047\u5982\u932f\u8aa4\u6301\u7e8c\u767c\u751f\uff0c\u8acb\u5148\u78ba\u5b9a\u60a8\u7684 Home Assistant \u7cfb\u7d71\u4e0a\u7684\u6642\u9593\u8a2d\u5b9a\u6b63\u78ba\u5f8c\uff0c\u518d\u8a66\u4e00\u6b21\u3002"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "\u6b32\u555f\u7528\u4e00\u6b21\u6027\u4e14\u5177\u6642\u6548\u6027\u7684\u5bc6\u78bc\u4e4b\u5169\u6b65\u9a5f\u9a57\u8b49\u529f\u80fd\uff0c\u8acb\u4f7f\u7528\u60a8\u7684\u9a57\u8b49 App \u6383\u7784\u4e0b\u65b9\u7684 QR code \u3002\u5018\u82e5\u60a8\u5c1a\u672a\u5b89\u88dd\u4efb\u4f55 App\uff0c\u63a8\u85a6\u60a8\u4f7f\u7528 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \u6216 [Authy](https://authy.com/)\u3002\n\n{qr_code}\n\n\u65bc\u6383\u63cf\u4e4b\u5f8c\uff0c\u8f38\u5165 App \u4e2d\u7684\u516d\u4f4d\u6578\u5b57\u9032\u884c\u8a2d\u5b9a\u9a57\u8b49\u3002\u5047\u5982\u6383\u63cf\u51fa\u73fe\u554f\u984c\uff0c\u8acb\u624b\u52d5\u8f38\u5165\u4ee5\u4e0b\u9a57\u8b49\u78bc **`{code}`**\u3002",
|
||||
"title": "\u4f7f\u7528 TOTP \u8a2d\u5b9a\u5169\u6b65\u9a5f\u9a57\u8b49"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -374,11 +374,11 @@ class XiaomiCube(XiaomiBinarySensor):
|
||||
self._last_action = None
|
||||
self._state = False
|
||||
if 'proto' not in device or int(device['proto'][0:1]) == 1:
|
||||
self._data_key = 'status'
|
||||
data_key = 'status'
|
||||
else:
|
||||
self._data_key = 'cube_status'
|
||||
data_key = 'cube_status'
|
||||
XiaomiBinarySensor.__init__(self, device, 'Cube', xiaomi_hub,
|
||||
None, None)
|
||||
data_key, None)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
|
||||
15
homeassistant/components/cast/.translations/fr.json
Normal file
15
homeassistant/components/cast/.translations/fr.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "Aucun appareil Google Cast trouv\u00e9 sur le r\u00e9seau.",
|
||||
"single_instance_allowed": "Seulement une seule configuration de Google Cast est n\u00e9cessaire."
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Voulez-vous configurer Google Cast?",
|
||||
"title": "Google Cast"
|
||||
}
|
||||
},
|
||||
"title": "Google Cast"
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant import core
|
||||
from homeassistant.components import http
|
||||
from homeassistant.components.conversation.util import create_matcher
|
||||
from homeassistant.components.http.data_validator import (
|
||||
RequestDataValidator)
|
||||
from homeassistant.components.cover import (INTENT_OPEN_COVER,
|
||||
@@ -74,7 +75,7 @@ def async_register(hass, intent_type, utterances):
|
||||
if isinstance(utterance, REGEX_TYPE):
|
||||
conf.append(utterance)
|
||||
else:
|
||||
conf.append(_create_matcher(utterance))
|
||||
conf.append(create_matcher(utterance))
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
@@ -91,7 +92,7 @@ async def async_setup(hass, config):
|
||||
if conf is None:
|
||||
conf = intents[intent_type] = []
|
||||
|
||||
conf.extend(_create_matcher(utterance) for utterance in utterances)
|
||||
conf.extend(create_matcher(utterance) for utterance in utterances)
|
||||
|
||||
async def process(service):
|
||||
"""Parse text into commands."""
|
||||
@@ -146,39 +147,6 @@ async def async_setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
def _create_matcher(utterance):
|
||||
"""Create a regex that matches the utterance."""
|
||||
# Split utterance into parts that are type: NORMAL, GROUP or OPTIONAL
|
||||
# Pattern matches (GROUP|OPTIONAL): Change light to [the color] {name}
|
||||
parts = re.split(r'({\w+}|\[[\w\s]+\] *)', utterance)
|
||||
# Pattern to extract name from GROUP part. Matches {name}
|
||||
group_matcher = re.compile(r'{(\w+)}')
|
||||
# Pattern to extract text from OPTIONAL part. Matches [the color]
|
||||
optional_matcher = re.compile(r'\[([\w ]+)\] *')
|
||||
|
||||
pattern = ['^']
|
||||
for part in parts:
|
||||
group_match = group_matcher.match(part)
|
||||
optional_match = optional_matcher.match(part)
|
||||
|
||||
# Normal part
|
||||
if group_match is None and optional_match is None:
|
||||
pattern.append(part)
|
||||
continue
|
||||
|
||||
# Group part
|
||||
if group_match is not None:
|
||||
pattern.append(
|
||||
r'(?P<{}>[\w ]+?)\s*'.format(group_match.groups()[0]))
|
||||
|
||||
# Optional part
|
||||
elif optional_match is not None:
|
||||
pattern.append(r'(?:{} *)?'.format(optional_match.groups()[0]))
|
||||
|
||||
pattern.append('$')
|
||||
return re.compile(''.join(pattern), re.I)
|
||||
|
||||
|
||||
async def _process(hass, text):
|
||||
"""Process a line of text."""
|
||||
intents = hass.data.get(DOMAIN, {})
|
||||
|
||||
35
homeassistant/components/conversation/util.py
Normal file
35
homeassistant/components/conversation/util.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Util for Conversation."""
|
||||
import re
|
||||
|
||||
|
||||
def create_matcher(utterance):
|
||||
"""Create a regex that matches the utterance."""
|
||||
# Split utterance into parts that are type: NORMAL, GROUP or OPTIONAL
|
||||
# Pattern matches (GROUP|OPTIONAL): Change light to [the color] {name}
|
||||
parts = re.split(r'({\w+}|\[[\w\s]+\] *)', utterance)
|
||||
# Pattern to extract name from GROUP part. Matches {name}
|
||||
group_matcher = re.compile(r'{(\w+)}')
|
||||
# Pattern to extract text from OPTIONAL part. Matches [the color]
|
||||
optional_matcher = re.compile(r'\[([\w ]+)\] *')
|
||||
|
||||
pattern = ['^']
|
||||
for part in parts:
|
||||
group_match = group_matcher.match(part)
|
||||
optional_match = optional_matcher.match(part)
|
||||
|
||||
# Normal part
|
||||
if group_match is None and optional_match is None:
|
||||
pattern.append(part)
|
||||
continue
|
||||
|
||||
# Group part
|
||||
if group_match is not None:
|
||||
pattern.append(
|
||||
r'(?P<{}>[\w ]+?)\s*'.format(group_match.groups()[0]))
|
||||
|
||||
# Optional part
|
||||
elif optional_match is not None:
|
||||
pattern.append(r'(?:{} *)?'.format(optional_match.groups()[0]))
|
||||
|
||||
pattern.append('$')
|
||||
return re.compile(''.join(pattern), re.I)
|
||||
@@ -28,6 +28,6 @@
|
||||
"title": "Extra configuratieopties voor deCONZ"
|
||||
}
|
||||
},
|
||||
"title": "deCONZ"
|
||||
"title": "deCONZ Zigbee gateway"
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,6 @@
|
||||
"title": "Ekstra konfigurasjonsalternativer for deCONZ"
|
||||
}
|
||||
},
|
||||
"title": "deCONZ"
|
||||
"title": "deCONZ Zigbee gateway"
|
||||
}
|
||||
}
|
||||
@@ -143,7 +143,7 @@ class FeedManager:
|
||||
else:
|
||||
self._has_published_parsed = False
|
||||
_LOGGER.debug("No published_parsed info available for entry %s",
|
||||
entry.title)
|
||||
entry)
|
||||
entry.update({'feed_url': self._url})
|
||||
self._hass.bus.fire(self._event_type, entry)
|
||||
|
||||
@@ -164,7 +164,7 @@ class FeedManager:
|
||||
self._update_and_fire_entry(entry)
|
||||
new_entries = True
|
||||
else:
|
||||
_LOGGER.debug("Entry %s already processed", entry.title)
|
||||
_LOGGER.debug("Entry %s already processed", entry)
|
||||
if not new_entries:
|
||||
self._log_no_entries()
|
||||
self._firstrun = False
|
||||
|
||||
@@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.yaml import load_yaml
|
||||
|
||||
REQUIREMENTS = ['home-assistant-frontend==20180826.0']
|
||||
REQUIREMENTS = ['home-assistant-frontend==20180903.0']
|
||||
|
||||
DOMAIN = 'frontend'
|
||||
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log',
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"unknown": "Ein unbekannter Fehler ist aufgetreten."
|
||||
},
|
||||
"error": {
|
||||
"invalid_2fa": "Ung\u00fcltige 2-Faktor Authentifizierung, bitte versuche es erneut.",
|
||||
"invalid_2fa_method": "Ung\u00fcltige 2FA Methode (mit Telefon verifizieren)",
|
||||
"invalid_login": "Ung\u00fcltige Daten, bitte erneut versuchen."
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"unknown": "Unknown error occurred."
|
||||
},
|
||||
"error": {
|
||||
"invalid_2fa": "Invalid 2 Factor Authorization, please try again.",
|
||||
"invalid_2fa": "Invalid 2 Factor Authentication, please try again.",
|
||||
"invalid_2fa_method": "Invalid 2FA Method (Verify on Phone).",
|
||||
"invalid_login": "Invalid Login, please try again."
|
||||
},
|
||||
@@ -14,7 +14,7 @@
|
||||
"data": {
|
||||
"2fa": "2FA Pin"
|
||||
},
|
||||
"title": "2-Factor-Authorization"
|
||||
"title": "2-Factor-Authentication"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
|
||||
18
homeassistant/components/hangouts/.translations/es-419.json
Normal file
18
homeassistant/components/hangouts/.translations/es-419.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Google Hangouts ya est\u00e1 configurado",
|
||||
"unknown": "Se produjo un error desconocido."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"email": "Direcci\u00f3n de correo electr\u00f3nico",
|
||||
"password": "Contrase\u00f1a"
|
||||
},
|
||||
"title": "Inicio de sesi\u00f3n de Google Hangouts"
|
||||
}
|
||||
},
|
||||
"title": "Google Hangouts"
|
||||
}
|
||||
}
|
||||
24
homeassistant/components/hangouts/.translations/fr.json
Normal file
24
homeassistant/components/hangouts/.translations/fr.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Google Hangouts est d\u00e9j\u00e0 configur\u00e9",
|
||||
"unknown": "Une erreur inconnue s'est produite"
|
||||
},
|
||||
"error": {
|
||||
"invalid_login": "Login invalide, veuillez r\u00e9essayer."
|
||||
},
|
||||
"step": {
|
||||
"2fa": {
|
||||
"title": "Authentification \u00e0 2 facteurs"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"email": "Adresse e-mail",
|
||||
"password": "Mot de passe"
|
||||
},
|
||||
"title": "Connexion \u00e0 Google Hangouts"
|
||||
}
|
||||
},
|
||||
"title": "Google Hangouts"
|
||||
}
|
||||
}
|
||||
29
homeassistant/components/hangouts/.translations/hu.json
Normal file
29
homeassistant/components/hangouts/.translations/hu.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "A Google Hangouts m\u00e1r konfigur\u00e1lva van",
|
||||
"unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt."
|
||||
},
|
||||
"error": {
|
||||
"invalid_2fa": "\u00c9rv\u00e9nytelen K\u00e9tfaktoros hiteles\u00edt\u00e9s, pr\u00f3b\u00e1ld \u00fajra.",
|
||||
"invalid_2fa_method": "\u00c9rv\u00e9nytelen 2FA M\u00f3dszer (Ellen\u0151rz\u00e9s a Telefonon).",
|
||||
"invalid_login": "\u00c9rv\u00e9nytelen bejelentkez\u00e9s, pr\u00f3b\u00e1ld \u00fajra."
|
||||
},
|
||||
"step": {
|
||||
"2fa": {
|
||||
"data": {
|
||||
"2fa": "2FA Pin"
|
||||
},
|
||||
"title": "K\u00e9tfaktoros Hiteles\u00edt\u00e9s"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"email": "E-Mail C\u00edm",
|
||||
"password": "Jelsz\u00f3"
|
||||
},
|
||||
"title": "Google Hangouts Bejelentkez\u00e9s"
|
||||
}
|
||||
},
|
||||
"title": "Google Hangouts"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,29 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Google Hangouts \u00e8 gi\u00e0 configurato",
|
||||
"unknown": "Si \u00e8 verificato un errore sconosciuto."
|
||||
},
|
||||
"error": {
|
||||
"invalid_2fa": "Autenticazione a 2 fattori non valida, riprovare.",
|
||||
"invalid_2fa_method": "Metodo 2FA non valido (verifica sul telefono).",
|
||||
"invalid_login": "Accesso non valido, si prega di riprovare."
|
||||
},
|
||||
"step": {
|
||||
"2fa": {
|
||||
"data": {
|
||||
"2fa": "2FA Pin"
|
||||
},
|
||||
"title": "Autenticazione a due fattori"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"email": "Indirizzo email",
|
||||
"password": "Password"
|
||||
},
|
||||
"title": "Accesso a Google Hangouts"
|
||||
}
|
||||
},
|
||||
"title": "Google Hangouts"
|
||||
}
|
||||
}
|
||||
31
homeassistant/components/hangouts/.translations/ko.json
Normal file
31
homeassistant/components/hangouts/.translations/ko.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Google Hangouts \uc740 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4",
|
||||
"unknown": "\uc54c \uc218\uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"error": {
|
||||
"invalid_2fa": "2\ub2e8\uacc4 \uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574 \uc8fc\uc138\uc694.",
|
||||
"invalid_2fa_method": "2\ub2e8\uacc4 \uc778\uc99d \ubc29\ubc95\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. (\uc804\ud654\uae30\uc5d0\uc11c \ud655\uc778)",
|
||||
"invalid_login": "\uc798\ubabb\ub41c \ub85c\uadf8\uc778\uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694."
|
||||
},
|
||||
"step": {
|
||||
"2fa": {
|
||||
"data": {
|
||||
"2fa": "2\ub2e8\uacc4 \uc778\uc99d PIN"
|
||||
},
|
||||
"description": "\uc8c4\uc1a1\ud569\ub2c8\ub2e4. \uad00\ub828 \ub0b4\uc6a9\uc774 \uc544\uc9c1 \uc5c5\ub370\uc774\ud2b8 \ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \ucd94\ud6c4\uc5d0 \ubc18\uc601\ub420 \uc608\uc815\uc774\ub2c8 \uc870\uae08\ub9cc \uae30\ub2e4\ub824\uc8fc\uc138\uc694.",
|
||||
"title": "2\ub2e8\uacc4 \uc778\uc99d"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"email": "\uc774\uba54\uc77c \uc8fc\uc18c",
|
||||
"password": "\ube44\ubc00\ubc88\ud638"
|
||||
},
|
||||
"description": "\uc8c4\uc1a1\ud569\ub2c8\ub2e4. \uad00\ub828 \ub0b4\uc6a9\uc774 \uc544\uc9c1 \uc5c5\ub370\uc774\ud2b8 \ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \ucd94\ud6c4\uc5d0 \ubc18\uc601\ub420 \uc608\uc815\uc774\ub2c8 \uc870\uae08\ub9cc \uae30\ub2e4\ub824\uc8fc\uc138\uc694.",
|
||||
"title": "Google Hangouts \ub85c\uadf8\uc778"
|
||||
}
|
||||
},
|
||||
"title": "Google Hangouts"
|
||||
}
|
||||
}
|
||||
31
homeassistant/components/hangouts/.translations/lb.json
Normal file
31
homeassistant/components/hangouts/.translations/lb.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Google Hangouts ass scho konfigur\u00e9iert",
|
||||
"unknown": "Onbekannten Fehler opgetrueden"
|
||||
},
|
||||
"error": {
|
||||
"invalid_2fa": "Ong\u00eblteg 2-Faktor Authentifikatioun, prob\u00e9iert w.e.g. nach emol.",
|
||||
"invalid_2fa_method": "Ong\u00eblteg 2FA Methode (Iwwerpr\u00e9ift et um Telefon)",
|
||||
"invalid_login": "Ong\u00ebltege Login, prob\u00e9iert w.e.g. nach emol."
|
||||
},
|
||||
"step": {
|
||||
"2fa": {
|
||||
"data": {
|
||||
"2fa": "2FA Pin"
|
||||
},
|
||||
"description": "Eidel",
|
||||
"title": "2-Faktor-Authentifikatioun"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"email": "E-Mail Adress",
|
||||
"password": "Passwuert"
|
||||
},
|
||||
"description": "Eidel",
|
||||
"title": "Google Hangouts Login"
|
||||
}
|
||||
},
|
||||
"title": "Google Hangouts"
|
||||
}
|
||||
}
|
||||
29
homeassistant/components/hangouts/.translations/nl.json
Normal file
29
homeassistant/components/hangouts/.translations/nl.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Google Hangouts is al geconfigureerd",
|
||||
"unknown": "Onbekende fout opgetreden."
|
||||
},
|
||||
"error": {
|
||||
"invalid_2fa": "Ongeldige twee-factor-authenticatie, probeer het opnieuw.",
|
||||
"invalid_2fa_method": "Ongeldige 2FA-methode (verifi\u00ebren op telefoon).",
|
||||
"invalid_login": "Ongeldige aanmelding, probeer het opnieuw."
|
||||
},
|
||||
"step": {
|
||||
"2fa": {
|
||||
"data": {
|
||||
"2fa": "2FA pin"
|
||||
},
|
||||
"title": "Twee-factor-authenticatie"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"email": "E-mailadres",
|
||||
"password": "Wachtwoord"
|
||||
},
|
||||
"title": "Google Hangouts inlog"
|
||||
}
|
||||
},
|
||||
"title": "Google Hangouts"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,27 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Google Hangouts er allerede konfigurert",
|
||||
"unknown": "Ukjent feil oppstod."
|
||||
},
|
||||
"error": {
|
||||
"invalid_2fa": "Ugyldig tofaktorautentisering, vennligst pr\u00f8v igjen.",
|
||||
"invalid_2fa_method": "Ugyldig 2FA-metode (Bekreft p\u00e5 telefon).",
|
||||
"invalid_login": "Ugyldig innlogging, vennligst pr\u00f8v igjen."
|
||||
},
|
||||
"step": {
|
||||
"2fa": {
|
||||
"data": {
|
||||
"2fa": "2FA Pin"
|
||||
},
|
||||
"title": "Tofaktorautentisering"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"email": "E-postadresse",
|
||||
"password": "Passord"
|
||||
}
|
||||
},
|
||||
"title": "Google Hangouts p\u00e5logging"
|
||||
}
|
||||
},
|
||||
"title": "Google Hangouts"
|
||||
|
||||
@@ -5,19 +5,14 @@
|
||||
"unknown": "Ocorreu um erro desconhecido."
|
||||
},
|
||||
"error": {
|
||||
"invalid_2fa_method": "M\u00e9todo 2FA inv\u00e1lido (verificar no telefone).",
|
||||
"invalid_login": "Login inv\u00e1lido, por favor, tente novamente."
|
||||
"invalid_2fa": "Autentica\u00e7\u00e3o de 2 fatores inv\u00e1lida, por favor, tente novamente."
|
||||
},
|
||||
"step": {
|
||||
"2fa": {
|
||||
"data": {
|
||||
"2fa": "Pin 2FA"
|
||||
},
|
||||
"title": ""
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"email": "Endere\u00e7o de e-mail",
|
||||
"password": "Senha"
|
||||
},
|
||||
"title": "Login do Hangouts do Google"
|
||||
|
||||
31
homeassistant/components/hangouts/.translations/pt.json
Normal file
31
homeassistant/components/hangouts/.translations/pt.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Google Hangouts j\u00e1 est\u00e1 configurado",
|
||||
"unknown": "Ocorreu um erro desconhecido."
|
||||
},
|
||||
"error": {
|
||||
"invalid_2fa": "Autoriza\u00e7\u00e3o por 2 factores inv\u00e1lida, por favor, tente novamente.",
|
||||
"invalid_2fa_method": "M\u00e9todo 2FA inv\u00e1lido (verificar no telefone).",
|
||||
"invalid_login": "Login inv\u00e1lido, por favor, tente novamente."
|
||||
},
|
||||
"step": {
|
||||
"2fa": {
|
||||
"data": {
|
||||
"2fa": "Pin 2FA"
|
||||
},
|
||||
"description": "Vazio",
|
||||
"title": ""
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"email": "Endere\u00e7o de e-mail",
|
||||
"password": "Palavra-passe"
|
||||
},
|
||||
"description": "Vazio",
|
||||
"title": "Login Google Hangouts"
|
||||
}
|
||||
},
|
||||
"title": ""
|
||||
}
|
||||
}
|
||||
29
homeassistant/components/hangouts/.translations/zh-Hans.json
Normal file
29
homeassistant/components/hangouts/.translations/zh-Hans.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Google Hangouts \u5df2\u914d\u7f6e\u5b8c\u6210",
|
||||
"unknown": "\u53d1\u751f\u672a\u77e5\u9519\u8bef\u3002"
|
||||
},
|
||||
"error": {
|
||||
"invalid_2fa": "\u53cc\u91cd\u8ba4\u8bc1\u5931\u8d25\uff0c\u8bf7\u91cd\u8bd5\u3002",
|
||||
"invalid_2fa_method": "\u65e0\u6548\u7684\u53cc\u91cd\u8ba4\u8bc1\u65b9\u6cd5\uff08\u7535\u8bdd\u9a8c\u8bc1\uff09\u3002",
|
||||
"invalid_login": "\u767b\u9646\u5931\u8d25\uff0c\u8bf7\u518d\u8bd5\u4e00\u6b21\u3002"
|
||||
},
|
||||
"step": {
|
||||
"2fa": {
|
||||
"data": {
|
||||
"2fa": "2FA Pin"
|
||||
},
|
||||
"title": "\u53cc\u91cd\u8ba4\u8bc1"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"email": "\u7535\u5b50\u90ae\u4ef6\u5730\u5740",
|
||||
"password": "\u5bc6\u7801"
|
||||
},
|
||||
"title": "\u767b\u5f55 Google Hangouts"
|
||||
}
|
||||
},
|
||||
"title": "Google Hangouts"
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"unknown": "\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002"
|
||||
},
|
||||
"error": {
|
||||
"invalid_2fa": "\u5169\u968e\u6bb5\u9a57\u8b49\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002",
|
||||
"invalid_2fa": "\u5169\u6b65\u9a5f\u9a57\u8b49\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002",
|
||||
"invalid_2fa_method": "\u8a8d\u8b49\u65b9\u5f0f\u7121\u6548\uff08\u65bc\u96fb\u8a71\u4e0a\u9a57\u8b49\uff09\u3002",
|
||||
"invalid_login": "\u767b\u5165\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002"
|
||||
},
|
||||
@@ -15,7 +15,7 @@
|
||||
"2fa": "\u8a8d\u8b49\u78bc"
|
||||
},
|
||||
"description": "\u7a7a\u767d",
|
||||
"title": "\u5169\u968e\u6bb5\u8a8d\u8b49"
|
||||
"title": "\u5169\u6b65\u9a5f\u9a57\u8b49"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
|
||||
@@ -11,28 +11,62 @@ import voluptuous as vol
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.helpers import dispatcher
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .config_flow import configured_hangouts
|
||||
from .const import (
|
||||
CONF_BOT, CONF_COMMANDS, CONF_REFRESH_TOKEN, DOMAIN,
|
||||
CONF_BOT, CONF_INTENTS, CONF_REFRESH_TOKEN, DOMAIN,
|
||||
EVENT_HANGOUTS_CONNECTED, EVENT_HANGOUTS_CONVERSATIONS_CHANGED,
|
||||
MESSAGE_SCHEMA, SERVICE_SEND_MESSAGE,
|
||||
SERVICE_UPDATE)
|
||||
SERVICE_UPDATE, CONF_SENTENCES, CONF_MATCHERS,
|
||||
CONF_ERROR_SUPPRESSED_CONVERSATIONS, INTENT_SCHEMA, TARGETS_SCHEMA)
|
||||
|
||||
# We need an import from .config_flow, without it .config_flow is never loaded.
|
||||
from .config_flow import HangoutsFlowHandler # noqa: F401
|
||||
|
||||
|
||||
REQUIREMENTS = ['hangups==0.4.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_INTENTS, default={}): vol.Schema({
|
||||
cv.string: INTENT_SCHEMA
|
||||
}),
|
||||
vol.Optional(CONF_ERROR_SUPPRESSED_CONVERSATIONS, default=[]):
|
||||
[TARGETS_SCHEMA]
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the Hangouts bot component."""
|
||||
config = config.get(DOMAIN, {})
|
||||
hass.data[DOMAIN] = {CONF_COMMANDS: config.get(CONF_COMMANDS, [])}
|
||||
from homeassistant.components.conversation import create_matcher
|
||||
|
||||
if configured_hangouts(hass) is None:
|
||||
hass.async_add_job(hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={'source': config_entries.SOURCE_IMPORT}
|
||||
))
|
||||
config = config.get(DOMAIN)
|
||||
if config is None:
|
||||
hass.data[DOMAIN] = {
|
||||
CONF_INTENTS: {},
|
||||
CONF_ERROR_SUPPRESSED_CONVERSATIONS: [],
|
||||
}
|
||||
return True
|
||||
|
||||
hass.data[DOMAIN] = {
|
||||
CONF_INTENTS: config[CONF_INTENTS],
|
||||
CONF_ERROR_SUPPRESSED_CONVERSATIONS:
|
||||
config[CONF_ERROR_SUPPRESSED_CONVERSATIONS],
|
||||
}
|
||||
|
||||
for data in hass.data[DOMAIN][CONF_INTENTS].values():
|
||||
matchers = []
|
||||
for sentence in data[CONF_SENTENCES]:
|
||||
matchers.append(create_matcher(sentence))
|
||||
|
||||
data[CONF_MATCHERS] = matchers
|
||||
|
||||
hass.async_create_task(hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={'source': config_entries.SOURCE_IMPORT}
|
||||
))
|
||||
|
||||
return True
|
||||
|
||||
@@ -47,7 +81,8 @@ async def async_setup_entry(hass, config):
|
||||
bot = HangoutsBot(
|
||||
hass,
|
||||
config.data.get(CONF_REFRESH_TOKEN),
|
||||
hass.data[DOMAIN][CONF_COMMANDS])
|
||||
hass.data[DOMAIN][CONF_INTENTS],
|
||||
hass.data[DOMAIN][CONF_ERROR_SUPPRESSED_CONVERSATIONS])
|
||||
hass.data[DOMAIN][CONF_BOT] = bot
|
||||
except GoogleAuthError as exception:
|
||||
_LOGGER.error("Hangouts failed to log in: %s", str(exception))
|
||||
@@ -62,6 +97,10 @@ async def async_setup_entry(hass, config):
|
||||
hass,
|
||||
EVENT_HANGOUTS_CONVERSATIONS_CHANGED,
|
||||
bot.async_update_conversation_commands)
|
||||
dispatcher.async_dispatcher_connect(
|
||||
hass,
|
||||
EVENT_HANGOUTS_CONVERSATIONS_CHANGED,
|
||||
bot.async_handle_update_error_suppressed_conversations)
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
|
||||
bot.async_handle_hass_stop)
|
||||
|
||||
@@ -4,7 +4,6 @@ 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')
|
||||
@@ -18,17 +17,18 @@ CONF_BOT = 'bot'
|
||||
|
||||
CONF_CONVERSATIONS = 'conversations'
|
||||
CONF_DEFAULT_CONVERSATIONS = 'default_conversations'
|
||||
CONF_ERROR_SUPPRESSED_CONVERSATIONS = 'error_suppressed_conversations'
|
||||
|
||||
CONF_COMMANDS = 'commands'
|
||||
CONF_WORD = 'word'
|
||||
CONF_EXPRESSION = 'expression'
|
||||
|
||||
EVENT_HANGOUTS_COMMAND = 'hangouts_command'
|
||||
CONF_INTENTS = 'intents'
|
||||
CONF_INTENT_TYPE = 'intent_type'
|
||||
CONF_SENTENCES = 'sentences'
|
||||
CONF_MATCHERS = 'matchers'
|
||||
|
||||
EVENT_HANGOUTS_CONNECTED = 'hangouts_connected'
|
||||
EVENT_HANGOUTS_DISCONNECTED = 'hangouts_disconnected'
|
||||
EVENT_HANGOUTS_USERS_CHANGED = 'hangouts_users_changed'
|
||||
EVENT_HANGOUTS_CONVERSATIONS_CHANGED = 'hangouts_conversations_changed'
|
||||
EVENT_HANGOUTS_MESSAGE_RECEIVED = 'hangouts_message_received'
|
||||
|
||||
CONF_CONVERSATION_ID = 'id'
|
||||
CONF_CONVERSATION_NAME = 'name'
|
||||
@@ -59,20 +59,10 @@ MESSAGE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_MESSAGE): [MESSAGE_SEGMENT_SCHEMA]
|
||||
})
|
||||
|
||||
COMMAND_SCHEMA = vol.All(
|
||||
INTENT_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.Required(CONF_SENTENCES): vol.All(cv.ensure_list, [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)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
"""The Hangouts Bot."""
|
||||
import logging
|
||||
import re
|
||||
|
||||
from homeassistant.helpers import dispatcher
|
||||
from homeassistant.helpers import dispatcher, intent
|
||||
|
||||
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)
|
||||
ATTR_MESSAGE, ATTR_TARGET, CONF_CONVERSATIONS, DOMAIN,
|
||||
EVENT_HANGOUTS_CONNECTED, EVENT_HANGOUTS_CONVERSATIONS_CHANGED,
|
||||
EVENT_HANGOUTS_DISCONNECTED, EVENT_HANGOUTS_MESSAGE_RECEIVED,
|
||||
CONF_MATCHERS, CONF_CONVERSATION_ID,
|
||||
CONF_CONVERSATION_NAME)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -15,20 +16,34 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class HangoutsBot:
|
||||
"""The Hangouts Bot."""
|
||||
|
||||
def __init__(self, hass, refresh_token, commands):
|
||||
def __init__(self, hass, refresh_token, intents, error_suppressed_convs):
|
||||
"""Set up the client."""
|
||||
self.hass = hass
|
||||
self._connected = False
|
||||
|
||||
self._refresh_token = refresh_token
|
||||
|
||||
self._commands = commands
|
||||
self._intents = intents
|
||||
self._conversation_intents = None
|
||||
|
||||
self._word_commands = None
|
||||
self._expression_commands = None
|
||||
self._client = None
|
||||
self._user_list = None
|
||||
self._conversation_list = None
|
||||
self._error_suppressed_convs = error_suppressed_convs
|
||||
self._error_suppressed_conv_ids = None
|
||||
|
||||
dispatcher.async_dispatcher_connect(
|
||||
self.hass, EVENT_HANGOUTS_MESSAGE_RECEIVED,
|
||||
self._async_handle_conversation_message)
|
||||
|
||||
def _resolve_conversation_id(self, obj):
|
||||
if CONF_CONVERSATION_ID in obj:
|
||||
return obj[CONF_CONVERSATION_ID]
|
||||
if CONF_CONVERSATION_NAME in obj:
|
||||
conv = self._resolve_conversation_name(obj[CONF_CONVERSATION_NAME])
|
||||
if conv is not None:
|
||||
return conv.id_
|
||||
return None
|
||||
|
||||
def _resolve_conversation_name(self, name):
|
||||
for conv in self._conversation_list.get_all():
|
||||
@@ -38,89 +53,100 @@ class HangoutsBot:
|
||||
|
||||
def async_update_conversation_commands(self, _):
|
||||
"""Refresh the commands for every conversation."""
|
||||
self._word_commands = {}
|
||||
self._expression_commands = {}
|
||||
self._conversation_intents = {}
|
||||
|
||||
for command in self._commands:
|
||||
if command.get(CONF_CONVERSATIONS):
|
||||
for intent_type, data in self._intents.items():
|
||||
if data.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
|
||||
for conversation in data.get(CONF_CONVERSATIONS):
|
||||
conv_id = self._resolve_conversation_id(conversation)
|
||||
if conv_id is not None:
|
||||
conversations.append(conv_id)
|
||||
data['_' + CONF_CONVERSATIONS] = conversations
|
||||
else:
|
||||
command['_' + CONF_CONVERSATIONS] = \
|
||||
data['_' + 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 data['_' + CONF_CONVERSATIONS]:
|
||||
if conv_id not in self._conversation_intents:
|
||||
self._conversation_intents[conv_id] = {}
|
||||
|
||||
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)
|
||||
self._conversation_intents[conv_id][intent_type] = data
|
||||
|
||||
try:
|
||||
self._conversation_list.on_event.remove_observer(
|
||||
self._handle_conversation_event)
|
||||
self._async_handle_conversation_event)
|
||||
except ValueError:
|
||||
pass
|
||||
self._conversation_list.on_event.add_observer(
|
||||
self._handle_conversation_event)
|
||||
self._async_handle_conversation_event)
|
||||
|
||||
def _handle_conversation_event(self, event):
|
||||
def async_handle_update_error_suppressed_conversations(self, _):
|
||||
"""Resolve the list of error suppressed conversations."""
|
||||
self._error_suppressed_conv_ids = []
|
||||
for conversation in self._error_suppressed_convs:
|
||||
conv_id = self._resolve_conversation_id(conversation)
|
||||
if conv_id is not None:
|
||||
self._error_suppressed_conv_ids.append(conv_id)
|
||||
|
||||
async def _async_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)
|
||||
if isinstance(event, ChatMessageEvent):
|
||||
dispatcher.async_dispatcher_send(self.hass,
|
||||
EVENT_HANGOUTS_MESSAGE_RECEIVED,
|
||||
event.conversation_id,
|
||||
event.user_id, event)
|
||||
|
||||
def _handle_conversation_message(self, conv_id, user_id, event):
|
||||
async def _async_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
|
||||
message = event.text
|
||||
|
||||
_LOGGER.debug("Handling message '%s' from %s",
|
||||
event.text, user.full_name)
|
||||
message, user.full_name)
|
||||
|
||||
event_data = None
|
||||
intents = self._conversation_intents.get(conv_id)
|
||||
if intents is not None:
|
||||
is_error = False
|
||||
try:
|
||||
intent_result = await self._async_process(intents, message)
|
||||
except (intent.UnknownIntent, intent.IntentHandleError) as err:
|
||||
is_error = True
|
||||
intent_result = intent.IntentResponse()
|
||||
intent_result.async_set_speech(str(err))
|
||||
|
||||
if intent_result is None:
|
||||
is_error = True
|
||||
intent_result = intent.IntentResponse()
|
||||
intent_result.async_set_speech(
|
||||
"Sorry, I didn't understand that")
|
||||
|
||||
message = intent_result.as_dict().get('speech', {})\
|
||||
.get('plain', {}).get('speech')
|
||||
|
||||
if (message is not None) and not (
|
||||
is_error and conv_id in self._error_suppressed_conv_ids):
|
||||
await self._async_send_message(
|
||||
[{'text': message, 'parse_str': True}],
|
||||
[{CONF_CONVERSATION_ID: conv_id}])
|
||||
|
||||
async def _async_process(self, intents, text):
|
||||
"""Detect a matching intent."""
|
||||
for intent_type, data in intents.items():
|
||||
for matcher in data.get(CONF_MATCHERS, []):
|
||||
match = matcher.match(text)
|
||||
|
||||
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)
|
||||
|
||||
response = await self.hass.helpers.intent.async_handle(
|
||||
DOMAIN, intent_type,
|
||||
{key: {'value': value} for key, value
|
||||
in match.groupdict().items()}, text)
|
||||
return response
|
||||
|
||||
async def async_connect(self):
|
||||
"""Login to the Google Hangouts."""
|
||||
@@ -163,10 +189,12 @@ class HangoutsBot:
|
||||
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 CONF_CONVERSATION_ID in target:
|
||||
conversation = self._conversation_list.get(
|
||||
target[CONF_CONVERSATION_ID])
|
||||
elif CONF_CONVERSATION_NAME in target:
|
||||
conversation = self._resolve_conversation_name(
|
||||
target[CONF_CONVERSATION_NAME])
|
||||
if conversation is not None:
|
||||
conversations.append(conversation)
|
||||
|
||||
@@ -200,8 +228,8 @@ class HangoutsBot:
|
||||
users_in_conversation = []
|
||||
for user in conv.users:
|
||||
users_in_conversation.append(user.full_name)
|
||||
conversations[str(i)] = {'id': str(conv.id_),
|
||||
'name': conv.name,
|
||||
conversations[str(i)] = {CONF_CONVERSATION_ID: str(conv.id_),
|
||||
CONF_CONVERSATION_NAME: conv.name,
|
||||
'users': users_in_conversation}
|
||||
|
||||
self.hass.states.async_set("{}.conversations".format(DOMAIN),
|
||||
|
||||
@@ -869,7 +869,7 @@ class HMDevice(Entity):
|
||||
|
||||
# Availability has changed
|
||||
if attribute == 'UNREACH':
|
||||
self._available = bool(value)
|
||||
self._available = not bool(value)
|
||||
has_changed = True
|
||||
elif not self.available:
|
||||
self._available = False
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"abort": {
|
||||
"already_configured": "Accesspoint ya est\u00e1 configurado",
|
||||
"conection_aborted": "No se pudo conectar al servidor HMIP",
|
||||
"connection_aborted": "No se pudo conectar al servidor HMIP",
|
||||
"unknown": "Se produjo un error desconocido."
|
||||
},
|
||||
"error": {
|
||||
@@ -18,6 +19,7 @@
|
||||
"pin": "C\u00f3digo PIN (opcional)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "HomematicIP Cloud"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"unknown": "Une erreur inconnue s'est produite"
|
||||
},
|
||||
"error": {
|
||||
"invalid_pin": "Code PIN invalide, veuillez r\u00e9essayer.",
|
||||
"press_the_button": "Veuillez appuyer sur le bouton bleu.",
|
||||
"register_failed": "\u00c9chec d'enregistrement. Veuillez r\u00e9essayer."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"hapid": "ID du point d'acc\u00e8s (SGTIN)",
|
||||
"name": "Nom (facultatif, utilis\u00e9 comme pr\u00e9fixe de nom pour tous les p\u00e9riph\u00e9riques)",
|
||||
"pin": "Code PIN (facultatif)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,13 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Il punto di accesso \u00e8 gi\u00e0 configurato",
|
||||
"connection_aborted": "Impossibile connettersi al server HMIP"
|
||||
},
|
||||
"error": {
|
||||
"press_the_button": "Si prega di premere il pulsante blu.",
|
||||
"register_failed": "Registrazione fallita, si prega di riprovare."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Accesspoint is reeds geconfigureerd",
|
||||
"already_configured": "Accesspoint is al geconfigureerd",
|
||||
"conection_aborted": "Kon geen verbinding maken met de HMIP-server",
|
||||
"connection_aborted": "Kon geen verbinding maken met de HMIP-server",
|
||||
"unknown": "Er is een onbekende fout opgetreden."
|
||||
@@ -19,11 +19,11 @@
|
||||
"name": "Naam (optioneel, gebruikt als naamprefix voor alle apparaten)",
|
||||
"pin": "Pin-Code (optioneel)"
|
||||
},
|
||||
"title": "Kies HomematicIP Accesspoint"
|
||||
"title": "Kies HomematicIP accesspoint"
|
||||
},
|
||||
"link": {
|
||||
"description": "Druk op de blauwe knop op de accesspoint en de verzendknop om HomematicIP met de Home Assistant te registreren. \n\n",
|
||||
"title": "Link Accesspoint"
|
||||
"description": "Druk op de blauwe knop op het accesspoint en de verzendknop om HomematicIP bij Home Assistant te registreren. \n\n",
|
||||
"title": "Link accesspoint"
|
||||
}
|
||||
},
|
||||
"title": "HomematicIP Cloud"
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"title": "Velg HomematicIP tilgangspunkt"
|
||||
},
|
||||
"link": {
|
||||
"description": "Trykk p\u00e5 den bl\u00e5 knappen p\u00e5 tilgangspunktet og send knappen for \u00e5 registrere HomematicIP med Home Assistant. \n\n",
|
||||
"description": "Trykk p\u00e5 den bl\u00e5 knappen p\u00e5 tilgangspunktet og p\u00e5 send knappen for \u00e5 registrere HomematicIP med Home Assistant. \n\n",
|
||||
"title": "Link tilgangspunkt"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"abort": {
|
||||
"already_configured": "O ponto de acesso j\u00e1 se encontra configurado",
|
||||
"conection_aborted": "N\u00e3o foi poss\u00edvel ligar ao servidor HMIP",
|
||||
"connection_aborted": "N\u00e3o foi poss\u00edvel ligar ao servidor HMIP",
|
||||
"unknown": "Ocorreu um erro desconhecido."
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"abort": {
|
||||
"already_configured": "\u63a5\u5165\u70b9\u5df2\u7ecf\u914d\u7f6e\u5b8c\u6210",
|
||||
"conection_aborted": "\u65e0\u6cd5\u8fde\u63a5\u5230 HMIP \u670d\u52a1\u5668",
|
||||
"connection_aborted": "\u65e0\u6cd5\u8fde\u63a5\u5230 HMIP \u670d\u52a1\u5668",
|
||||
"unknown": "\u53d1\u751f\u672a\u77e5\u9519\u8bef\u3002"
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -200,18 +200,10 @@ class HomeAssistantHTTP:
|
||||
if is_ban_enabled:
|
||||
setup_bans(hass, app, login_threshold)
|
||||
|
||||
if hass.auth.active:
|
||||
if hass.auth.support_legacy:
|
||||
_LOGGER.warning("Experimental auth api enabled and "
|
||||
"legacy_api_password support enabled. Please "
|
||||
"use access_token instead api_password, "
|
||||
"although you can still use legacy "
|
||||
"api_password")
|
||||
else:
|
||||
_LOGGER.warning("Experimental auth api enabled. Please use "
|
||||
"access_token instead api_password.")
|
||||
elif api_password is None:
|
||||
_LOGGER.warning("You have been advised to set http.api_password.")
|
||||
if hass.auth.active and hass.auth.support_legacy:
|
||||
_LOGGER.warning(
|
||||
"legacy_api_password support has been enabled. If you don't "
|
||||
"require it, remove the 'api_password' from your http config.")
|
||||
|
||||
setup_auth(app, trusted_networks, hass.auth.active,
|
||||
support_legacy=hass.auth.support_legacy,
|
||||
|
||||
@@ -32,7 +32,7 @@ def setup_auth(app, trusted_networks, use_auth,
|
||||
if request.path not in old_auth_warning:
|
||||
_LOGGER.log(
|
||||
logging.INFO if support_legacy else logging.WARNING,
|
||||
'Please change to use bearer token access %s from %s',
|
||||
'You need to use a bearer token to access %s from %s',
|
||||
request.path, request[KEY_REAL_IP])
|
||||
old_auth_warning.add(request.path)
|
||||
|
||||
|
||||
@@ -24,6 +24,6 @@
|
||||
"title": "Link Hub"
|
||||
}
|
||||
},
|
||||
"title": "Philips Hue Bridge"
|
||||
"title": "Philips Hue"
|
||||
}
|
||||
}
|
||||
@@ -167,9 +167,9 @@ async def async_setup_platform(hass,
|
||||
return True
|
||||
|
||||
|
||||
def lifx_features(device):
|
||||
"""Return a feature map for this device, or a default map if unknown."""
|
||||
return aiolifx().products.features_map.get(device.product) or \
|
||||
def lifx_features(bulb):
|
||||
"""Return a feature map for this bulb, or a default map if unknown."""
|
||||
return aiolifx().products.features_map.get(bulb.product) or \
|
||||
aiolifx().products.features_map.get(1)
|
||||
|
||||
|
||||
@@ -256,7 +256,7 @@ class LIFXManager:
|
||||
|
||||
async def start_effect(self, entities, service, **kwargs):
|
||||
"""Start a light effect on entities."""
|
||||
devices = [light.device for light in entities]
|
||||
bulbs = [light.bulb for light in entities]
|
||||
|
||||
if service == SERVICE_EFFECT_PULSE:
|
||||
effect = aiolifx_effects().EffectPulse(
|
||||
@@ -266,7 +266,7 @@ class LIFXManager:
|
||||
mode=kwargs.get(ATTR_MODE),
|
||||
hsbk=find_hsbk(**kwargs),
|
||||
)
|
||||
await self.effects_conductor.start(effect, devices)
|
||||
await self.effects_conductor.start(effect, bulbs)
|
||||
elif service == SERVICE_EFFECT_COLORLOOP:
|
||||
preprocess_turn_on_alternatives(kwargs)
|
||||
|
||||
@@ -282,12 +282,12 @@ class LIFXManager:
|
||||
transition=kwargs.get(ATTR_TRANSITION),
|
||||
brightness=brightness,
|
||||
)
|
||||
await self.effects_conductor.start(effect, devices)
|
||||
await self.effects_conductor.start(effect, bulbs)
|
||||
elif service == SERVICE_EFFECT_STOP:
|
||||
await self.effects_conductor.stop(devices)
|
||||
await self.effects_conductor.stop(bulbs)
|
||||
|
||||
def service_to_entities(self, service):
|
||||
"""Return the known devices that a service call mentions."""
|
||||
"""Return the known entities that a service call mentions."""
|
||||
entity_ids = extract_entity_ids(self.hass, service)
|
||||
if entity_ids:
|
||||
entities = [entity for entity in self.entities.values()
|
||||
@@ -298,50 +298,50 @@ class LIFXManager:
|
||||
return entities
|
||||
|
||||
@callback
|
||||
def register(self, device):
|
||||
def register(self, bulb):
|
||||
"""Handle aiolifx detected bulb."""
|
||||
self.hass.async_add_job(self.register_new_device(device))
|
||||
self.hass.async_add_job(self.register_new_bulb(bulb))
|
||||
|
||||
async def register_new_device(self, device):
|
||||
async def register_new_bulb(self, bulb):
|
||||
"""Handle newly detected bulb."""
|
||||
if device.mac_addr in self.entities:
|
||||
entity = self.entities[device.mac_addr]
|
||||
if bulb.mac_addr in self.entities:
|
||||
entity = self.entities[bulb.mac_addr]
|
||||
entity.registered = True
|
||||
_LOGGER.debug("%s register AGAIN", entity.who)
|
||||
await entity.update_hass()
|
||||
else:
|
||||
_LOGGER.debug("%s register NEW", device.ip_addr)
|
||||
_LOGGER.debug("%s register NEW", bulb.ip_addr)
|
||||
|
||||
# Read initial state
|
||||
ack = AwaitAioLIFX().wait
|
||||
color_resp = await ack(device.get_color)
|
||||
color_resp = await ack(bulb.get_color)
|
||||
if color_resp:
|
||||
version_resp = await ack(device.get_version)
|
||||
version_resp = await ack(bulb.get_version)
|
||||
|
||||
if color_resp is None or version_resp is None:
|
||||
_LOGGER.error("Failed to initialize %s", device.ip_addr)
|
||||
device.registered = False
|
||||
_LOGGER.error("Failed to initialize %s", bulb.ip_addr)
|
||||
bulb.registered = False
|
||||
else:
|
||||
device.timeout = MESSAGE_TIMEOUT
|
||||
device.retry_count = MESSAGE_RETRIES
|
||||
device.unregister_timeout = UNAVAILABLE_GRACE
|
||||
bulb.timeout = MESSAGE_TIMEOUT
|
||||
bulb.retry_count = MESSAGE_RETRIES
|
||||
bulb.unregister_timeout = UNAVAILABLE_GRACE
|
||||
|
||||
if lifx_features(device)["multizone"]:
|
||||
entity = LIFXStrip(device, self.effects_conductor)
|
||||
elif lifx_features(device)["color"]:
|
||||
entity = LIFXColor(device, self.effects_conductor)
|
||||
if lifx_features(bulb)["multizone"]:
|
||||
entity = LIFXStrip(bulb, self.effects_conductor)
|
||||
elif lifx_features(bulb)["color"]:
|
||||
entity = LIFXColor(bulb, self.effects_conductor)
|
||||
else:
|
||||
entity = LIFXWhite(device, self.effects_conductor)
|
||||
entity = LIFXWhite(bulb, self.effects_conductor)
|
||||
|
||||
_LOGGER.debug("%s register READY", entity.who)
|
||||
self.entities[device.mac_addr] = entity
|
||||
self.entities[bulb.mac_addr] = entity
|
||||
self.async_add_entities([entity], True)
|
||||
|
||||
@callback
|
||||
def unregister(self, device):
|
||||
def unregister(self, bulb):
|
||||
"""Handle aiolifx disappearing bulbs."""
|
||||
if device.mac_addr in self.entities:
|
||||
entity = self.entities[device.mac_addr]
|
||||
if bulb.mac_addr in self.entities:
|
||||
entity = self.entities[bulb.mac_addr]
|
||||
_LOGGER.debug("%s unregister", entity.who)
|
||||
entity.registered = False
|
||||
self.hass.async_add_job(entity.async_update_ha_state())
|
||||
@@ -352,20 +352,17 @@ class AwaitAioLIFX:
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the wrapper."""
|
||||
self.device = None
|
||||
self.message = None
|
||||
self.event = asyncio.Event()
|
||||
|
||||
@callback
|
||||
def callback(self, device, message):
|
||||
def callback(self, bulb, message):
|
||||
"""Handle responses."""
|
||||
self.device = device
|
||||
self.message = message
|
||||
self.event.set()
|
||||
|
||||
async def wait(self, method):
|
||||
"""Call an aiolifx method and wait for its response."""
|
||||
self.device = None
|
||||
self.message = None
|
||||
self.event.clear()
|
||||
method(callb=self.callback)
|
||||
@@ -387,9 +384,9 @@ def convert_16_to_8(value):
|
||||
class LIFXLight(Light):
|
||||
"""Representation of a LIFX light."""
|
||||
|
||||
def __init__(self, device, effects_conductor):
|
||||
def __init__(self, bulb, effects_conductor):
|
||||
"""Initialize the light."""
|
||||
self.light = device
|
||||
self.bulb = bulb
|
||||
self.effects_conductor = effects_conductor
|
||||
self.registered = True
|
||||
self.postponed_update = None
|
||||
@@ -397,34 +394,34 @@ class LIFXLight(Light):
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return the availability of the device."""
|
||||
"""Return the availability of the bulb."""
|
||||
return self.registered
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID."""
|
||||
return self.light.mac_addr
|
||||
return self.bulb.mac_addr
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self.light.label
|
||||
"""Return the name of the bulb."""
|
||||
return self.bulb.label
|
||||
|
||||
@property
|
||||
def who(self):
|
||||
"""Return a string identifying the device."""
|
||||
return "%s (%s)" % (self.light.ip_addr, self.name)
|
||||
"""Return a string identifying the bulb."""
|
||||
return "%s (%s)" % (self.bulb.ip_addr, self.name)
|
||||
|
||||
@property
|
||||
def min_mireds(self):
|
||||
"""Return the coldest color_temp that this light supports."""
|
||||
kelvin = lifx_features(self.light)['max_kelvin']
|
||||
kelvin = lifx_features(self.bulb)['max_kelvin']
|
||||
return math.floor(color_util.color_temperature_kelvin_to_mired(kelvin))
|
||||
|
||||
@property
|
||||
def max_mireds(self):
|
||||
"""Return the warmest color_temp that this light supports."""
|
||||
kelvin = lifx_features(self.light)['min_kelvin']
|
||||
kelvin = lifx_features(self.bulb)['min_kelvin']
|
||||
return math.ceil(color_util.color_temperature_kelvin_to_mired(kelvin))
|
||||
|
||||
@property
|
||||
@@ -432,8 +429,8 @@ class LIFXLight(Light):
|
||||
"""Flag supported features."""
|
||||
support = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION | SUPPORT_EFFECT
|
||||
|
||||
device_features = lifx_features(self.light)
|
||||
if device_features['min_kelvin'] != device_features['max_kelvin']:
|
||||
bulb_features = lifx_features(self.bulb)
|
||||
if bulb_features['min_kelvin'] != bulb_features['max_kelvin']:
|
||||
support |= SUPPORT_COLOR_TEMP
|
||||
|
||||
return support
|
||||
@@ -441,25 +438,25 @@ class LIFXLight(Light):
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
return convert_16_to_8(self.light.color[2])
|
||||
return convert_16_to_8(self.bulb.color[2])
|
||||
|
||||
@property
|
||||
def color_temp(self):
|
||||
"""Return the color temperature."""
|
||||
_, sat, _, kelvin = self.light.color
|
||||
_, sat, _, kelvin = self.bulb.color
|
||||
if sat:
|
||||
return None
|
||||
return color_util.color_temperature_kelvin_to_mired(kelvin)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if device is on."""
|
||||
return self.light.power_level != 0
|
||||
"""Return true if light is on."""
|
||||
return self.bulb.power_level != 0
|
||||
|
||||
@property
|
||||
def effect(self):
|
||||
"""Return the name of the currently running effect."""
|
||||
effect = self.effects_conductor.effect(self.light)
|
||||
effect = self.effects_conductor.effect(self.bulb)
|
||||
if effect:
|
||||
return 'lifx_effect_' + effect.name
|
||||
return None
|
||||
@@ -485,19 +482,19 @@ class LIFXLight(Light):
|
||||
util.dt.utcnow() + timedelta(milliseconds=when))
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn the device on."""
|
||||
"""Turn the light on."""
|
||||
kwargs[ATTR_POWER] = True
|
||||
self.hass.async_add_job(self.set_state(**kwargs))
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Turn the device off."""
|
||||
"""Turn the light off."""
|
||||
kwargs[ATTR_POWER] = False
|
||||
self.hass.async_add_job(self.set_state(**kwargs))
|
||||
|
||||
async def set_state(self, **kwargs):
|
||||
"""Set a color on the light and turn it on/off."""
|
||||
async with self.lock:
|
||||
bulb = self.light
|
||||
bulb = self.bulb
|
||||
|
||||
await self.effects_conductor.stop([bulb])
|
||||
|
||||
@@ -544,13 +541,13 @@ class LIFXLight(Light):
|
||||
await self.update_during_transition(fade)
|
||||
|
||||
async def set_power(self, ack, pwr, duration=0):
|
||||
"""Send a power change to the device."""
|
||||
await ack(partial(self.light.set_power, pwr, duration=duration))
|
||||
"""Send a power change to the bulb."""
|
||||
await ack(partial(self.bulb.set_power, pwr, duration=duration))
|
||||
|
||||
async def set_color(self, ack, hsbk, kwargs, duration=0):
|
||||
"""Send a color change to the device."""
|
||||
hsbk = merge_hsbk(self.light.color, hsbk)
|
||||
await ack(partial(self.light.set_color, hsbk, duration=duration))
|
||||
"""Send a color change to the bulb."""
|
||||
hsbk = merge_hsbk(self.bulb.color, hsbk)
|
||||
await ack(partial(self.bulb.set_color, hsbk, duration=duration))
|
||||
|
||||
async def default_effect(self, **kwargs):
|
||||
"""Start an effect with default parameters."""
|
||||
@@ -563,7 +560,7 @@ class LIFXLight(Light):
|
||||
async def async_update(self):
|
||||
"""Update bulb status."""
|
||||
if self.available and not self.lock.locked():
|
||||
await AwaitAioLIFX().wait(self.light.get_color)
|
||||
await AwaitAioLIFX().wait(self.bulb.get_color)
|
||||
|
||||
|
||||
class LIFXWhite(LIFXLight):
|
||||
@@ -600,7 +597,7 @@ class LIFXColor(LIFXLight):
|
||||
@property
|
||||
def hs_color(self):
|
||||
"""Return the hs value."""
|
||||
hue, sat, _, _ = self.light.color
|
||||
hue, sat, _, _ = self.bulb.color
|
||||
hue = hue / 65535 * 360
|
||||
sat = sat / 65535 * 100
|
||||
return (hue, sat) if sat else None
|
||||
@@ -610,8 +607,8 @@ class LIFXStrip(LIFXColor):
|
||||
"""Representation of a LIFX light strip with multiple zones."""
|
||||
|
||||
async def set_color(self, ack, hsbk, kwargs, duration=0):
|
||||
"""Send a color change to the device."""
|
||||
bulb = self.light
|
||||
"""Send a color change to the bulb."""
|
||||
bulb = self.bulb
|
||||
num_zones = len(bulb.color_zones)
|
||||
|
||||
zones = kwargs.get(ATTR_ZONES)
|
||||
@@ -659,7 +656,7 @@ class LIFXStrip(LIFXColor):
|
||||
while self.available and zone < top:
|
||||
# Each get_color_zones can update 8 zones at once
|
||||
resp = await AwaitAioLIFX().wait(partial(
|
||||
self.light.get_color_zones,
|
||||
self.bulb.get_color_zones,
|
||||
start_index=zone))
|
||||
if resp:
|
||||
zone += 8
|
||||
|
||||
@@ -531,7 +531,7 @@ class PlexClient(MediaPlayerDevice):
|
||||
@property
|
||||
def device(self):
|
||||
"""Return the device, if any."""
|
||||
return self.device
|
||||
return self._device
|
||||
|
||||
@property
|
||||
def marked_unavailable(self):
|
||||
|
||||
23
homeassistant/components/nest/.translations/fr.json
Normal file
23
homeassistant/components/nest/.translations/fr.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_setup": "Vous ne pouvez configurer qu'un seul compte Nest."
|
||||
},
|
||||
"error": {
|
||||
"internal_error": "Erreur interne lors de la validation du code",
|
||||
"invalid_code": "Code invalide"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Fournisseur d'authentification"
|
||||
},
|
||||
"link": {
|
||||
"data": {
|
||||
"code": "Code PIN"
|
||||
},
|
||||
"title": "Lier un compte Nest"
|
||||
}
|
||||
},
|
||||
"title": "Nest"
|
||||
}
|
||||
}
|
||||
12
homeassistant/components/sensor/.translations/moon.hu.json
Normal file
12
homeassistant/components/sensor/.translations/moon.hu.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"state": {
|
||||
"first_quarter": "Els\u0151 negyed",
|
||||
"full_moon": "Telihold",
|
||||
"last_quarter": "Utols\u00f3 negyed",
|
||||
"new_moon": "\u00dajhold",
|
||||
"waning_crescent": "Fogy\u00f3 Hold (sarl\u00f3)",
|
||||
"waning_gibbous": "Fogy\u00f3 Hold",
|
||||
"waxing_crescent": "N\u00f6v\u0151 Hold (sarl\u00f3)",
|
||||
"waxing_gibbous": "N\u00f6v\u0151 Hold"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"state": {
|
||||
"first_quarter": "F\u00f8rste kvartdel",
|
||||
"first_quarter": "F\u00f8rste kvarter",
|
||||
"full_moon": "Fullm\u00e5ne",
|
||||
"last_quarter": "Siste kvartdel",
|
||||
"last_quarter": "Siste kvarter",
|
||||
"new_moon": "Nym\u00e5ne",
|
||||
"waning_crescent": "Minkende halvm\u00e5ne",
|
||||
"waning_gibbous": "Minkende trekvartm\u00e5ne",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"state": {
|
||||
"first_quarter": "Quarto crescente",
|
||||
"full_moon": "Cheia",
|
||||
"full_moon": "Lua cheia",
|
||||
"last_quarter": "Quarto minguante",
|
||||
"new_moon": "Nova",
|
||||
"new_moon": "Lua Nova",
|
||||
"waning_crescent": "Minguante",
|
||||
"waning_gibbous": "Minguante gibosa",
|
||||
"waxing_crescent": "Crescente",
|
||||
|
||||
12
homeassistant/components/sensor/.translations/moon.pt.json
Normal file
12
homeassistant/components/sensor/.translations/moon.pt.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"state": {
|
||||
"first_quarter": "Quarto crescente",
|
||||
"full_moon": "Lua cheia",
|
||||
"last_quarter": "Quarto minguante",
|
||||
"new_moon": "Lua nova",
|
||||
"waning_crescent": "Lua crescente",
|
||||
"waning_gibbous": "Minguante convexa",
|
||||
"waxing_crescent": "Lua minguante",
|
||||
"waxing_gibbous": "Crescente convexa"
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "M\u00f6chten Sie Sonos konfigurieren?",
|
||||
"description": "M\u00f6chten Sie Sonos einrichten?",
|
||||
"title": "Sonos"
|
||||
}
|
||||
},
|
||||
|
||||
13
homeassistant/components/sonos/.translations/fr.json
Normal file
13
homeassistant/components/sonos/.translations/fr.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "Aucun p\u00e9riph\u00e9rique Sonos trouv\u00e9 sur le r\u00e9seau.",
|
||||
"single_instance_allowed": "Seulement une seule configuration de Sonos est n\u00e9cessaire."
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Voulez-vous configurer Sonos?"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -287,7 +287,6 @@ class TelldusLiveEntity(Entity):
|
||||
self._id = device_id
|
||||
self._client = hass.data[DOMAIN]
|
||||
self._client.entities.append(self)
|
||||
self.device = self._client.device(device_id)
|
||||
self._name = self.device.name
|
||||
_LOGGER.debug('Created device %s', self)
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ SUPPORT_XIAOMI = SUPPORT_STATE | SUPPORT_PAUSE | \
|
||||
|
||||
|
||||
STATE_CODE_TO_STATE = {
|
||||
2: STATE_IDLE,
|
||||
3: STATE_IDLE,
|
||||
5: STATE_CLEANING,
|
||||
6: STATE_RETURNING,
|
||||
|
||||
@@ -124,14 +124,14 @@ def setup(hass, config):
|
||||
_LOGGER.error(
|
||||
'Unable to get description url for %s',
|
||||
'{}:{}'.format(host, port) if port else host)
|
||||
return False
|
||||
continue
|
||||
|
||||
try:
|
||||
device = pywemo.discovery.device_from_description(url, None)
|
||||
except (requests.exceptions.ConnectionError,
|
||||
requests.exceptions.Timeout) as err:
|
||||
_LOGGER.error('Unable to access %s (%s)', url, err)
|
||||
return False
|
||||
continue
|
||||
|
||||
devices.append((url, device))
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import re
|
||||
import shutil
|
||||
# pylint: disable=unused-import
|
||||
from typing import ( # noqa: F401
|
||||
Any, Tuple, Optional, Dict, List, Union, Callable)
|
||||
Any, Tuple, Optional, Dict, List, Union, Callable, Sequence, Set)
|
||||
from types import ModuleType
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
@@ -23,7 +23,7 @@ from homeassistant.const import (
|
||||
CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT, TEMP_CELSIUS,
|
||||
__version__, CONF_CUSTOMIZE, CONF_CUSTOMIZE_DOMAIN, CONF_CUSTOMIZE_GLOB,
|
||||
CONF_WHITELIST_EXTERNAL_DIRS, CONF_AUTH_PROVIDERS, CONF_AUTH_MFA_MODULES,
|
||||
CONF_TYPE)
|
||||
CONF_TYPE, CONF_ID)
|
||||
from homeassistant.core import callback, DOMAIN as CONF_CORE, HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.loader import get_component, get_platform
|
||||
@@ -128,6 +128,48 @@ some_password: welcome
|
||||
"""
|
||||
|
||||
|
||||
def _no_duplicate_auth_provider(configs: Sequence[Dict[str, Any]]) \
|
||||
-> Sequence[Dict[str, Any]]:
|
||||
"""No duplicate auth provider config allowed in a list.
|
||||
|
||||
Each type of auth provider can only have one config without optional id.
|
||||
Unique id is required if same type of auth provider used multiple times.
|
||||
"""
|
||||
config_keys = set() # type: Set[Tuple[str, Optional[str]]]
|
||||
for config in configs:
|
||||
key = (config[CONF_TYPE], config.get(CONF_ID))
|
||||
if key in config_keys:
|
||||
raise vol.Invalid(
|
||||
'Duplicate auth provider {} found. Please add unique IDs if '
|
||||
'you want to have the same auth provider twice'.format(
|
||||
config[CONF_TYPE]
|
||||
))
|
||||
config_keys.add(key)
|
||||
return configs
|
||||
|
||||
|
||||
def _no_duplicate_auth_mfa_module(configs: Sequence[Dict[str, Any]]) \
|
||||
-> Sequence[Dict[str, Any]]:
|
||||
"""No duplicate auth mfa module item allowed in a list.
|
||||
|
||||
Each type of mfa module can only have one config without optional id.
|
||||
A global unique id is required if same type of mfa module used multiple
|
||||
times.
|
||||
Note: this is different than auth provider
|
||||
"""
|
||||
config_keys = set() # type: Set[str]
|
||||
for config in configs:
|
||||
key = config.get(CONF_ID, config[CONF_TYPE])
|
||||
if key in config_keys:
|
||||
raise vol.Invalid(
|
||||
'Duplicate mfa module {} found. Please add unique IDs if '
|
||||
'you want to have the same mfa module twice'.format(
|
||||
config[CONF_TYPE]
|
||||
))
|
||||
config_keys.add(key)
|
||||
return configs
|
||||
|
||||
|
||||
PACKAGES_CONFIG_SCHEMA = vol.Schema({
|
||||
cv.slug: vol.Schema( # Package names are slugs
|
||||
{cv.slug: vol.Any(dict, list, None)}) # Only slugs for component names
|
||||
@@ -166,10 +208,16 @@ CORE_CONFIG_SCHEMA = CUSTOMIZE_CONFIG_SCHEMA.extend({
|
||||
CONF_TYPE: vol.NotIn(['insecure_example'],
|
||||
'The insecure_example auth provider'
|
||||
' is for testing only.')
|
||||
})]),
|
||||
})],
|
||||
_no_duplicate_auth_provider),
|
||||
vol.Optional(CONF_AUTH_MFA_MODULES):
|
||||
vol.All(cv.ensure_list,
|
||||
[auth_mfa_modules.MULTI_FACTOR_AUTH_MODULE_SCHEMA]),
|
||||
[auth_mfa_modules.MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({
|
||||
CONF_TYPE: vol.NotIn(['insecure_example'],
|
||||
'The insecure_example mfa module'
|
||||
' is for testing only.')
|
||||
})],
|
||||
_no_duplicate_auth_mfa_module),
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"""Constants used by Home Assistant components."""
|
||||
MAJOR_VERSION = 0
|
||||
MINOR_VERSION = 77
|
||||
PATCH_VERSION = '0b2'
|
||||
PATCH_VERSION = '3'
|
||||
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
|
||||
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
|
||||
REQUIRED_PYTHON_VER = (3, 5, 3)
|
||||
|
||||
@@ -13,6 +13,8 @@ pyyaml>=3.13,<4
|
||||
requests==2.19.1
|
||||
voluptuous==0.11.5
|
||||
|
||||
pycryptodome>=3.6.6
|
||||
|
||||
# Breaks Python 3.6 and is not needed for our supported Python versions
|
||||
enum34==1000000000.0.0
|
||||
|
||||
|
||||
@@ -73,11 +73,13 @@ def package_loadable(package: str) -> bool:
|
||||
# This is a zip file
|
||||
req = pkg_resources.Requirement.parse(urlparse(package).fragment)
|
||||
|
||||
req_proj_name = req.project_name.lower()
|
||||
|
||||
for path in sys.path:
|
||||
for dist in pkg_resources.find_distributions(path):
|
||||
# If the project name is the same, it will be the one that is
|
||||
# loaded when we import it.
|
||||
if dist.project_name == req.project_name:
|
||||
if dist.project_name.lower() == req_proj_name:
|
||||
return dist in req
|
||||
|
||||
return False
|
||||
|
||||
@@ -442,7 +442,7 @@ hole==0.3.0
|
||||
holidays==0.9.6
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20180826.0
|
||||
home-assistant-frontend==20180903.0
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
# homekit==0.10
|
||||
|
||||
@@ -84,7 +84,7 @@ hbmqtt==0.9.2
|
||||
holidays==0.9.6
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20180826.0
|
||||
home-assistant-frontend==20180903.0
|
||||
|
||||
# homeassistant.components.homematicip_cloud
|
||||
homematicip==0.9.8
|
||||
|
||||
@@ -124,6 +124,8 @@ URL_PIN = ('https://home-assistant.io/developers/code_review_platform/'
|
||||
CONSTRAINT_PATH = os.path.join(os.path.dirname(__file__),
|
||||
'../homeassistant/package_constraints.txt')
|
||||
CONSTRAINT_BASE = """
|
||||
pycryptodome>=3.6.6
|
||||
|
||||
# Breaks Python 3.6 and is not needed for our supported Python versions
|
||||
enum34==1000000000.0.0
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ async def test_login(hass):
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result['flow_id'], {'pin': 'invalid-code'})
|
||||
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result['errors']['base'] == 'invalid_auth'
|
||||
assert result['errors']['base'] == 'invalid_code'
|
||||
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result['flow_id'], {'pin': '123456'})
|
||||
|
||||
@@ -121,7 +121,7 @@ async def test_login_flow_validates_mfa(hass):
|
||||
result['flow_id'], {'code': 'invalid-code'})
|
||||
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result['step_id'] == 'mfa'
|
||||
assert result['errors']['base'] == 'invalid_auth'
|
||||
assert result['errors']['base'] == 'invalid_code'
|
||||
|
||||
with patch('pyotp.TOTP.verify', return_value=True):
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
|
||||
@@ -3,6 +3,7 @@ from unittest.mock import Mock
|
||||
|
||||
import base64
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.auth import auth_manager_from_config, auth_store
|
||||
@@ -111,11 +112,11 @@ async def test_saving_loading(data, hass):
|
||||
async def test_not_allow_set_id():
|
||||
"""Test we are not allowed to set an ID in config."""
|
||||
hass = Mock()
|
||||
provider = await auth_provider_from_config(hass, None, {
|
||||
'type': 'homeassistant',
|
||||
'id': 'invalid',
|
||||
})
|
||||
assert provider is None
|
||||
with pytest.raises(vol.Invalid):
|
||||
await auth_provider_from_config(hass, None, {
|
||||
'type': 'homeassistant',
|
||||
'id': 'invalid',
|
||||
})
|
||||
|
||||
|
||||
async def test_new_users_populate_values(hass, data):
|
||||
|
||||
@@ -74,16 +74,16 @@ async def test_login_flow(manager, provider):
|
||||
# trusted network didn't loaded
|
||||
flow = await provider.async_login_flow({'ip_address': '127.0.0.1'})
|
||||
step = await flow.async_step_init()
|
||||
assert step['step_id'] == 'init'
|
||||
assert step['errors']['base'] == 'invalid_auth'
|
||||
assert step['type'] == 'abort'
|
||||
assert step['reason'] == 'not_whitelisted'
|
||||
|
||||
provider.hass.http = Mock(trusted_networks=['192.168.0.1'])
|
||||
|
||||
# not from trusted network
|
||||
flow = await provider.async_login_flow({'ip_address': '127.0.0.1'})
|
||||
step = await flow.async_step_init()
|
||||
assert step['step_id'] == 'init'
|
||||
assert step['errors']['base'] == 'invalid_auth'
|
||||
assert step['type'] == 'abort'
|
||||
assert step['reason'] == 'not_whitelisted'
|
||||
|
||||
# from trusted network, list users
|
||||
flow = await provider.async_login_flow({'ip_address': '192.168.0.1'})
|
||||
@@ -95,11 +95,6 @@ async def test_login_flow(manager, provider):
|
||||
with pytest.raises(vol.Invalid):
|
||||
assert schema({'user': 'invalid-user'})
|
||||
|
||||
# login with invalid user
|
||||
step = await flow.async_step_init({'user': 'invalid-user'})
|
||||
assert step['step_id'] == 'init'
|
||||
assert step['errors']['base'] == 'invalid_auth'
|
||||
|
||||
# login with valid user
|
||||
step = await flow.async_step_init({'user': user.id})
|
||||
assert step['type'] == 'create_entry'
|
||||
|
||||
@@ -3,6 +3,7 @@ from datetime import timedelta
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import auth, data_entry_flow
|
||||
from homeassistant.auth import (
|
||||
@@ -21,33 +22,36 @@ def mock_hass(loop):
|
||||
return hass
|
||||
|
||||
|
||||
async def test_auth_manager_from_config_validates_config_and_id(mock_hass):
|
||||
async def test_auth_manager_from_config_validates_config(mock_hass):
|
||||
"""Test get auth providers."""
|
||||
with pytest.raises(vol.Invalid):
|
||||
manager = await auth.auth_manager_from_config(mock_hass, [{
|
||||
'name': 'Test Name',
|
||||
'type': 'insecure_example',
|
||||
'users': [],
|
||||
}, {
|
||||
'name': 'Invalid config because no users',
|
||||
'type': 'insecure_example',
|
||||
'id': 'invalid_config',
|
||||
}], [])
|
||||
|
||||
manager = await auth.auth_manager_from_config(mock_hass, [{
|
||||
'name': 'Test Name',
|
||||
'type': 'insecure_example',
|
||||
'users': [],
|
||||
}, {
|
||||
'name': 'Invalid config because no users',
|
||||
'type': 'insecure_example',
|
||||
'id': 'invalid_config',
|
||||
}, {
|
||||
'name': 'Test Name 2',
|
||||
'type': 'insecure_example',
|
||||
'id': 'another',
|
||||
'users': [],
|
||||
}, {
|
||||
'name': 'Wrong because duplicate ID',
|
||||
'type': 'insecure_example',
|
||||
'id': 'another',
|
||||
'users': [],
|
||||
}], [])
|
||||
|
||||
providers = [{
|
||||
'name': provider.name,
|
||||
'id': provider.id,
|
||||
'type': provider.type,
|
||||
} for provider in manager.auth_providers]
|
||||
'name': provider.name,
|
||||
'id': provider.id,
|
||||
'type': provider.type,
|
||||
} for provider in manager.auth_providers]
|
||||
|
||||
assert providers == [{
|
||||
'name': 'Test Name',
|
||||
'type': 'insecure_example',
|
||||
@@ -61,6 +65,26 @@ async def test_auth_manager_from_config_validates_config_and_id(mock_hass):
|
||||
|
||||
async def test_auth_manager_from_config_auth_modules(mock_hass):
|
||||
"""Test get auth modules."""
|
||||
with pytest.raises(vol.Invalid):
|
||||
manager = await auth.auth_manager_from_config(mock_hass, [{
|
||||
'name': 'Test Name',
|
||||
'type': 'insecure_example',
|
||||
'users': [],
|
||||
}, {
|
||||
'name': 'Test Name 2',
|
||||
'type': 'insecure_example',
|
||||
'id': 'another',
|
||||
'users': [],
|
||||
}], [{
|
||||
'name': 'Module 1',
|
||||
'type': 'insecure_example',
|
||||
'data': [],
|
||||
}, {
|
||||
'name': 'Invalid config because no data',
|
||||
'type': 'insecure_example',
|
||||
'id': 'another',
|
||||
}])
|
||||
|
||||
manager = await auth.auth_manager_from_config(mock_hass, [{
|
||||
'name': 'Test Name',
|
||||
'type': 'insecure_example',
|
||||
@@ -79,13 +103,7 @@ async def test_auth_manager_from_config_auth_modules(mock_hass):
|
||||
'type': 'insecure_example',
|
||||
'id': 'another',
|
||||
'data': [],
|
||||
}, {
|
||||
'name': 'Duplicate ID',
|
||||
'type': 'insecure_example',
|
||||
'id': 'another',
|
||||
'data': [],
|
||||
}])
|
||||
|
||||
providers = [{
|
||||
'name': provider.name,
|
||||
'type': provider.type,
|
||||
@@ -410,10 +428,10 @@ async def test_login_with_auth_module(mock_hass):
|
||||
'pin': 'invalid-pin',
|
||||
})
|
||||
|
||||
# Invalid auth error
|
||||
# Invalid code error
|
||||
assert step['type'] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert step['step_id'] == 'mfa'
|
||||
assert step['errors'] == {'base': 'invalid_auth'}
|
||||
assert step['errors'] == {'base': 'invalid_code'}
|
||||
|
||||
step = await manager.login_flow.async_configure(step['flow_id'], {
|
||||
'pin': 'test-pin',
|
||||
@@ -553,18 +571,9 @@ async def test_auth_module_expired_session(mock_hass):
|
||||
step = await manager.login_flow.async_configure(step['flow_id'], {
|
||||
'pin': 'test-pin',
|
||||
})
|
||||
# Invalid auth due session timeout
|
||||
assert step['type'] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert step['step_id'] == 'mfa'
|
||||
assert step['errors']['base'] == 'login_expired'
|
||||
|
||||
# The second try will fail as well
|
||||
step = await manager.login_flow.async_configure(step['flow_id'], {
|
||||
'pin': 'test-pin',
|
||||
})
|
||||
assert step['type'] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert step['step_id'] == 'mfa'
|
||||
assert step['errors']['base'] == 'login_expired'
|
||||
# login flow abort due session timeout
|
||||
assert step['type'] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert step['reason'] == 'login_expired'
|
||||
|
||||
|
||||
async def test_enable_mfa_for_user(hass, hass_storage):
|
||||
|
||||
@@ -290,11 +290,11 @@ async def test_http_api_wrong_data(hass, aiohttp_client):
|
||||
def test_create_matcher():
|
||||
"""Test the create matcher method."""
|
||||
# Basic sentence
|
||||
pattern = conversation._create_matcher('Hello world')
|
||||
pattern = conversation.create_matcher('Hello world')
|
||||
assert pattern.match('Hello world') is not None
|
||||
|
||||
# Match a part
|
||||
pattern = conversation._create_matcher('Hello {name}')
|
||||
pattern = conversation.create_matcher('Hello {name}')
|
||||
match = pattern.match('hello world')
|
||||
assert match is not None
|
||||
assert match.groupdict()['name'] == 'world'
|
||||
@@ -302,7 +302,7 @@ def test_create_matcher():
|
||||
assert no_match is None
|
||||
|
||||
# Optional and matching part
|
||||
pattern = conversation._create_matcher('Turn on [the] {name}')
|
||||
pattern = conversation.create_matcher('Turn on [the] {name}')
|
||||
match = pattern.match('turn on the kitchen lights')
|
||||
assert match is not None
|
||||
assert match.groupdict()['name'] == 'kitchen lights'
|
||||
@@ -313,7 +313,7 @@ def test_create_matcher():
|
||||
assert match is None
|
||||
|
||||
# Two different optional parts, 1 matching part
|
||||
pattern = conversation._create_matcher('Turn on [the] [a] {name}')
|
||||
pattern = conversation.create_matcher('Turn on [the] [a] {name}')
|
||||
match = pattern.match('turn on the kitchen lights')
|
||||
assert match is not None
|
||||
assert match.groupdict()['name'] == 'kitchen lights'
|
||||
@@ -325,13 +325,13 @@ def test_create_matcher():
|
||||
assert match.groupdict()['name'] == 'kitchen light'
|
||||
|
||||
# Strip plural
|
||||
pattern = conversation._create_matcher('Turn {name}[s] on')
|
||||
pattern = conversation.create_matcher('Turn {name}[s] on')
|
||||
match = pattern.match('turn kitchen lights on')
|
||||
assert match is not None
|
||||
assert match.groupdict()['name'] == 'kitchen light'
|
||||
|
||||
# Optional 2 words
|
||||
pattern = conversation._create_matcher('Turn [the great] {name} on')
|
||||
pattern = conversation.create_matcher('Turn [the great] {name} on')
|
||||
match = pattern.match('turn the great kitchen lights on')
|
||||
assert match is not None
|
||||
assert match.groupdict()['name'] == 'kitchen lights'
|
||||
|
||||
@@ -160,11 +160,11 @@ class TestFeedreaderComponent(unittest.TestCase):
|
||||
manager, events = self.setup_manager(feed_data, max_entries=5)
|
||||
assert len(events) == 5
|
||||
|
||||
def test_feed_without_publication_date(self):
|
||||
"""Test simple feed with entry without publication date."""
|
||||
def test_feed_without_publication_date_and_title(self):
|
||||
"""Test simple feed with entry without publication date and title."""
|
||||
feed_data = load_fixture('feedreader3.xml')
|
||||
manager, events = self.setup_manager(feed_data)
|
||||
assert len(events) == 2
|
||||
assert len(events) == 3
|
||||
|
||||
def test_feed_invalid_data(self):
|
||||
"""Test feed with invalid data."""
|
||||
|
||||
5
tests/fixtures/feedreader3.xml
vendored
5
tests/fixtures/feedreader3.xml
vendored
@@ -21,6 +21,11 @@
|
||||
<link>http://www.example.com/link/2</link>
|
||||
<guid isPermaLink="false">GUID 2</guid>
|
||||
</item>
|
||||
<item>
|
||||
<description>Description 3</description>
|
||||
<link>http://www.example.com/link/3</link>
|
||||
<guid isPermaLink="false">GUID 3</guid>
|
||||
</item>
|
||||
|
||||
</channel>
|
||||
</rss>
|
||||
|
||||
@@ -895,9 +895,73 @@ async def test_disallowed_auth_provider_config(hass):
|
||||
'name': 'Huis',
|
||||
CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL,
|
||||
'time_zone': 'GMT',
|
||||
CONF_AUTH_PROVIDERS: [
|
||||
{'type': 'insecure_example'},
|
||||
]
|
||||
CONF_AUTH_PROVIDERS: [{
|
||||
'type': 'insecure_example',
|
||||
'users': [{
|
||||
'username': 'test-user',
|
||||
'password': 'test-pass',
|
||||
'name': 'Test Name'
|
||||
}],
|
||||
}]
|
||||
}
|
||||
with pytest.raises(Invalid):
|
||||
await config_util.async_process_ha_core_config(hass, core_config)
|
||||
|
||||
|
||||
async def test_disallowed_duplicated_auth_provider_config(hass):
|
||||
"""Test loading insecure example auth provider is disallowed."""
|
||||
core_config = {
|
||||
'latitude': 60,
|
||||
'longitude': 50,
|
||||
'elevation': 25,
|
||||
'name': 'Huis',
|
||||
CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL,
|
||||
'time_zone': 'GMT',
|
||||
CONF_AUTH_PROVIDERS: [{
|
||||
'type': 'homeassistant',
|
||||
}, {
|
||||
'type': 'homeassistant',
|
||||
}]
|
||||
}
|
||||
with pytest.raises(Invalid):
|
||||
await config_util.async_process_ha_core_config(hass, core_config)
|
||||
|
||||
|
||||
async def test_disallowed_auth_mfa_module_config(hass):
|
||||
"""Test loading insecure example auth mfa module is disallowed."""
|
||||
core_config = {
|
||||
'latitude': 60,
|
||||
'longitude': 50,
|
||||
'elevation': 25,
|
||||
'name': 'Huis',
|
||||
CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL,
|
||||
'time_zone': 'GMT',
|
||||
CONF_AUTH_MFA_MODULES: [{
|
||||
'type': 'insecure_example',
|
||||
'data': [{
|
||||
'user_id': 'mock-user',
|
||||
'pin': 'test-pin'
|
||||
}]
|
||||
}]
|
||||
}
|
||||
with pytest.raises(Invalid):
|
||||
await config_util.async_process_ha_core_config(hass, core_config)
|
||||
|
||||
|
||||
async def test_disallowed_duplicated_auth_mfa_module_config(hass):
|
||||
"""Test loading insecure example auth mfa module is disallowed."""
|
||||
core_config = {
|
||||
'latitude': 60,
|
||||
'longitude': 50,
|
||||
'elevation': 25,
|
||||
'name': 'Huis',
|
||||
CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL,
|
||||
'time_zone': 'GMT',
|
||||
CONF_AUTH_MFA_MODULES: [{
|
||||
'type': 'totp',
|
||||
}, {
|
||||
'type': 'totp',
|
||||
}]
|
||||
}
|
||||
with pytest.raises(Invalid):
|
||||
await config_util.async_process_ha_core_config(hass, core_config)
|
||||
|
||||
@@ -239,3 +239,6 @@ def test_package_loadable_installed_twice():
|
||||
|
||||
with patch('pkg_resources.find_distributions', side_effect=[[v2]]):
|
||||
assert package.package_loadable('hello==2.0.0')
|
||||
|
||||
with patch('pkg_resources.find_distributions', side_effect=[[v2]]):
|
||||
assert package.package_loadable('Hello==2.0.0')
|
||||
|
||||
Reference in New Issue
Block a user