forked from home-assistant/core
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8413101148 | ||
|
|
8fb66c351e | ||
|
|
16ad9c2ae6 | ||
|
|
2ad938ed44 | ||
|
|
969b15a297 | ||
|
|
c8449d8f8a | ||
|
|
6992a6fe6d | ||
|
|
2ece671bfd | ||
|
|
c13e5fcb92 | ||
|
|
3783d1ce90 |
@@ -249,13 +249,13 @@ class AuthManager:
|
||||
|
||||
await module.async_depose_user(user.id)
|
||||
|
||||
async def async_get_enabled_mfa(self, user: models.User) -> List[str]:
|
||||
async def async_get_enabled_mfa(self, user: models.User) -> Dict[str, str]:
|
||||
"""List enabled mfa modules for user."""
|
||||
module_ids = []
|
||||
modules = OrderedDict() # type: Dict[str, str]
|
||||
for module_id, module in self._mfa_modules.items():
|
||||
if await module.async_is_user_setup(user.id):
|
||||
module_ids.append(module_id)
|
||||
return module_ids
|
||||
modules[module_id] = module.name
|
||||
return modules
|
||||
|
||||
async def async_create_refresh_token(self, user: models.User,
|
||||
client_id: Optional[str] = None) \
|
||||
|
||||
212
homeassistant/auth/mfa_modules/totp.py
Normal file
212
homeassistant/auth/mfa_modules/totp.py
Normal file
@@ -0,0 +1,212 @@
|
||||
"""Time-based One Time Password auth module."""
|
||||
import logging
|
||||
from io import BytesIO
|
||||
from typing import Any, Dict, Optional, Tuple # noqa: F401
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.auth.models import User
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
|
||||
MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow
|
||||
|
||||
REQUIREMENTS = ['pyotp==2.2.6', 'PyQRCode==1.2.1']
|
||||
|
||||
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_KEY = 'auth_module.totp'
|
||||
STORAGE_USERS = 'users'
|
||||
STORAGE_USER_ID = 'user_id'
|
||||
STORAGE_OTA_SECRET = 'ota_secret'
|
||||
|
||||
INPUT_FIELD_CODE = 'code'
|
||||
|
||||
DUMMY_SECRET = 'FPPTH34D4E3MI2HG'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _generate_qr_code(data: str) -> str:
|
||||
"""Generate a base64 PNG string represent QR Code image of data."""
|
||||
import pyqrcode
|
||||
|
||||
qr_code = pyqrcode.create(data)
|
||||
|
||||
with BytesIO() as buffer:
|
||||
qr_code.svg(file=buffer, scale=4)
|
||||
return '{}'.format(
|
||||
buffer.getvalue().decode("ascii").replace('\n', '')
|
||||
.replace('<?xml version="1.0" encoding="UTF-8"?>'
|
||||
'<svg xmlns="http://www.w3.org/2000/svg"', '<svg')
|
||||
)
|
||||
|
||||
|
||||
def _generate_secret_and_qr_code(username: str) -> Tuple[str, str, str]:
|
||||
"""Generate a secret, url, and QR code."""
|
||||
import pyotp
|
||||
|
||||
ota_secret = pyotp.random_base32()
|
||||
url = pyotp.totp.TOTP(ota_secret).provisioning_uri(
|
||||
username, issuer_name="Home Assistant")
|
||||
image = _generate_qr_code(url)
|
||||
return ota_secret, url, image
|
||||
|
||||
|
||||
@MULTI_FACTOR_AUTH_MODULES.register('totp')
|
||||
class TotpAuthModule(MultiFactorAuthModule):
|
||||
"""Auth module validate time-based one time password."""
|
||||
|
||||
DEFAULT_TITLE = 'Time-based One Time Password'
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
|
||||
"""Initialize the user data store."""
|
||||
super().__init__(hass, config)
|
||||
self._users = None # type: Optional[Dict[str, str]]
|
||||
self._user_store = hass.helpers.storage.Store(
|
||||
STORAGE_VERSION, STORAGE_KEY)
|
||||
|
||||
@property
|
||||
def input_schema(self) -> vol.Schema:
|
||||
"""Validate login flow input data."""
|
||||
return vol.Schema({INPUT_FIELD_CODE: str})
|
||||
|
||||
async def _async_load(self) -> None:
|
||||
"""Load stored data."""
|
||||
data = await self._user_store.async_load()
|
||||
|
||||
if data is None:
|
||||
data = {STORAGE_USERS: {}}
|
||||
|
||||
self._users = data.get(STORAGE_USERS, {})
|
||||
|
||||
async def _async_save(self) -> None:
|
||||
"""Save data."""
|
||||
await self._user_store.async_save({STORAGE_USERS: self._users})
|
||||
|
||||
def _add_ota_secret(self, user_id: str,
|
||||
secret: Optional[str] = None) -> str:
|
||||
"""Create a ota_secret for user."""
|
||||
import pyotp
|
||||
|
||||
ota_secret = secret or pyotp.random_base32() # type: str
|
||||
|
||||
self._users[user_id] = ota_secret # type: ignore
|
||||
return ota_secret
|
||||
|
||||
async def async_setup_flow(self, user_id: str) -> SetupFlow:
|
||||
"""Return a data entry flow handler for setup module.
|
||||
|
||||
Mfa module should extend SetupFlow
|
||||
"""
|
||||
user = await self.hass.auth.async_get_user(user_id) # type: ignore
|
||||
return TotpSetupFlow(self, self.input_schema, user)
|
||||
|
||||
async def async_setup_user(self, user_id: str, setup_data: Any) -> str:
|
||||
"""Set up auth module for user."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
|
||||
result = await self.hass.async_add_executor_job(
|
||||
self._add_ota_secret, user_id, setup_data.get('secret'))
|
||||
|
||||
await self._async_save()
|
||||
return result
|
||||
|
||||
async def async_depose_user(self, user_id: str) -> None:
|
||||
"""Depose auth module for user."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
|
||||
if self._users.pop(user_id, None): # type: ignore
|
||||
await self._async_save()
|
||||
|
||||
async def async_is_user_setup(self, user_id: str) -> bool:
|
||||
"""Return whether user is setup."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
|
||||
return user_id in self._users # type: ignore
|
||||
|
||||
async def async_validation(
|
||||
self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
||||
"""Return True if validation passed."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
|
||||
# user_input has been validate in caller
|
||||
return await self.hass.async_add_executor_job(
|
||||
self._validate_2fa, user_id, user_input[INPUT_FIELD_CODE])
|
||||
|
||||
def _validate_2fa(self, user_id: str, code: str) -> bool:
|
||||
"""Validate two factor authentication code."""
|
||||
import pyotp
|
||||
|
||||
ota_secret = self._users.get(user_id) # type: ignore
|
||||
if ota_secret is None:
|
||||
# even we cannot find user, we still do verify
|
||||
# to make timing the same as if user was found.
|
||||
pyotp.TOTP(DUMMY_SECRET).verify(code)
|
||||
return False
|
||||
|
||||
return bool(pyotp.TOTP(ota_secret).verify(code))
|
||||
|
||||
|
||||
class TotpSetupFlow(SetupFlow):
|
||||
"""Handler for the setup flow."""
|
||||
|
||||
def __init__(self, auth_module: TotpAuthModule,
|
||||
setup_schema: vol.Schema,
|
||||
user: User) -> None:
|
||||
"""Initialize the setup flow."""
|
||||
super().__init__(auth_module, setup_schema, user.id)
|
||||
# to fix typing complaint
|
||||
self._auth_module = auth_module # type: TotpAuthModule
|
||||
self._user = user
|
||||
self._ota_secret = None # type: Optional[str]
|
||||
self._url = None # type Optional[str]
|
||||
self._image = None # type Optional[str]
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None) \
|
||||
-> Dict[str, Any]:
|
||||
"""Handle the first step of setup flow.
|
||||
|
||||
Return self.async_show_form(step_id='init') if user_input == None.
|
||||
Return self.async_create_entry(data={'result': result}) if finish.
|
||||
"""
|
||||
import pyotp
|
||||
|
||||
errors = {} # type: Dict[str, str]
|
||||
|
||||
if user_input:
|
||||
verified = await self.hass.async_add_executor_job( # type: ignore
|
||||
pyotp.TOTP(self._ota_secret).verify, user_input['code'])
|
||||
if verified:
|
||||
result = await self._auth_module.async_setup_user(
|
||||
self._user_id, {'secret': self._ota_secret})
|
||||
return self.async_create_entry(
|
||||
title=self._auth_module.name,
|
||||
data={'result': result}
|
||||
)
|
||||
|
||||
errors['base'] = 'invalid_code'
|
||||
|
||||
else:
|
||||
hass = self._auth_module.hass
|
||||
self._ota_secret, self._url, self._image = \
|
||||
await hass.async_add_executor_job( # type: ignore
|
||||
_generate_secret_and_qr_code, str(self._user.name))
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=self._setup_schema,
|
||||
description_placeholders={
|
||||
'code': self._ota_secret,
|
||||
'url': self._url,
|
||||
'qr_code': self._image
|
||||
},
|
||||
errors=errors
|
||||
)
|
||||
@@ -168,7 +168,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||
self._auth_provider = auth_provider
|
||||
self._auth_module_id = None # type: Optional[str]
|
||||
self._auth_manager = auth_provider.hass.auth # type: ignore
|
||||
self.available_mfa_modules = [] # type: List
|
||||
self.available_mfa_modules = {} # type: Dict[str, str]
|
||||
self.created_at = dt_util.utcnow()
|
||||
self.user = None # type: Optional[User]
|
||||
|
||||
@@ -196,7 +196,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||
errors['base'] = 'invalid_auth_module'
|
||||
|
||||
if len(self.available_mfa_modules) == 1:
|
||||
self._auth_module_id = self.available_mfa_modules[0]
|
||||
self._auth_module_id = list(self.available_mfa_modules.keys())[0]
|
||||
return await self.async_step_mfa()
|
||||
|
||||
return self.async_show_form(
|
||||
|
||||
@@ -5,13 +5,16 @@ import hashlib
|
||||
import hmac
|
||||
from typing import Any, Dict, List, Optional, cast
|
||||
|
||||
import bcrypt
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_ID
|
||||
from homeassistant.core import callback, HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.util.async_ import run_coroutine_threadsafe
|
||||
|
||||
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
|
||||
|
||||
from ..models import Credentials, UserMeta
|
||||
from ..util import generate_secret
|
||||
|
||||
@@ -74,8 +77,7 @@ class Data:
|
||||
|
||||
Raises InvalidAuth if auth invalid.
|
||||
"""
|
||||
hashed = self.hash_password(password)
|
||||
|
||||
dummy = b'$2b$12$CiuFGszHx9eNHxPuQcwBWez4CwDTOcLTX5CbOpV6gef2nYuXkY7BO'
|
||||
found = None
|
||||
|
||||
# Compare all users to avoid timing attacks.
|
||||
@@ -84,22 +86,55 @@ class Data:
|
||||
found = user
|
||||
|
||||
if found is None:
|
||||
# Do one more compare to make timing the same as if user was found.
|
||||
hmac.compare_digest(hashed, hashed)
|
||||
# check a hash to make timing the same as if user was found
|
||||
bcrypt.checkpw(b'foo',
|
||||
dummy)
|
||||
raise InvalidAuth
|
||||
|
||||
if not hmac.compare_digest(hashed,
|
||||
base64.b64decode(found['password'])):
|
||||
user_hash = base64.b64decode(found['password'])
|
||||
|
||||
# if the hash is not a bcrypt hash...
|
||||
# provide a transparant upgrade for old pbkdf2 hash format
|
||||
if not (user_hash.startswith(b'$2a$')
|
||||
or user_hash.startswith(b'$2b$')
|
||||
or user_hash.startswith(b'$2x$')
|
||||
or user_hash.startswith(b'$2y$')):
|
||||
# IMPORTANT! validate the login, bail if invalid
|
||||
hashed = self.legacy_hash_password(password)
|
||||
if not hmac.compare_digest(hashed, user_hash):
|
||||
raise InvalidAuth
|
||||
# then re-hash the valid password with bcrypt
|
||||
self.change_password(found['username'], password)
|
||||
run_coroutine_threadsafe(
|
||||
self.async_save(), self.hass.loop
|
||||
).result()
|
||||
user_hash = base64.b64decode(found['password'])
|
||||
|
||||
# bcrypt.checkpw is timing-safe
|
||||
if not bcrypt.checkpw(password.encode(),
|
||||
user_hash):
|
||||
raise InvalidAuth
|
||||
|
||||
def hash_password(self, password: str, for_storage: bool = False) -> bytes:
|
||||
"""Encode a password."""
|
||||
def legacy_hash_password(self, password: str,
|
||||
for_storage: bool = False) -> bytes:
|
||||
"""LEGACY password encoding."""
|
||||
# We're no longer storing salts in data, but if one exists we
|
||||
# should be able to retrieve it.
|
||||
salt = self._data['salt'].encode() # type: ignore
|
||||
hashed = hashlib.pbkdf2_hmac('sha512', password.encode(), salt, 100000)
|
||||
if for_storage:
|
||||
hashed = base64.b64encode(hashed)
|
||||
return hashed
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def hash_password(self, password: str, for_storage: bool = False) -> bytes:
|
||||
"""Encode a password."""
|
||||
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12)) \
|
||||
# type: bytes
|
||||
if for_storage:
|
||||
hashed = base64.b64encode(hashed)
|
||||
return hashed
|
||||
|
||||
def add_auth(self, username: str, password: str) -> None:
|
||||
"""Add a new authenticated user/pass."""
|
||||
if any(user['username'] == username for user in self.users):
|
||||
|
||||
16
homeassistant/components/auth/.translations/en.json
Normal file
16
homeassistant/components/auth/.translations/en.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"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."
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
homeassistant/components/auth/strings.json
Normal file
16
homeassistant/components/auth/strings.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"mfa_setup":{
|
||||
"totp": {
|
||||
"title": "TOTP",
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Set up two-factor authentication using TOTP",
|
||||
"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}`**."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,7 +105,7 @@ class KNXBinarySensor(BinarySensorDevice):
|
||||
|
||||
def __init__(self, hass, device):
|
||||
"""Initialize of KNX binary sensor."""
|
||||
self._device = device
|
||||
self.device = device
|
||||
self.hass = hass
|
||||
self.async_register_callbacks()
|
||||
self.automations = []
|
||||
@@ -116,12 +116,12 @@ class KNXBinarySensor(BinarySensorDevice):
|
||||
async def after_update_callback(device):
|
||||
"""Call after device was updated."""
|
||||
await self.async_update_ha_state()
|
||||
self._device.register_device_updated_cb(after_update_callback)
|
||||
self.device.register_device_updated_cb(after_update_callback)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the KNX device."""
|
||||
return self._device.name
|
||||
return self.device.name
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
@@ -136,9 +136,9 @@ class KNXBinarySensor(BinarySensorDevice):
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return self._device.device_class
|
||||
return self.device.device_class
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self._device.is_on()
|
||||
return self.device.is_on()
|
||||
|
||||
@@ -130,7 +130,7 @@ class NestBinarySensor(NestSensorDevice, BinarySensorDevice):
|
||||
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
value = getattr(self._device, self.variable)
|
||||
value = getattr(self.device, self.variable)
|
||||
if self.variable in STRUCTURE_BINARY_TYPES:
|
||||
self._state = bool(STRUCTURE_BINARY_STATE_MAP
|
||||
[self.variable].get(value))
|
||||
@@ -154,5 +154,4 @@ class NestActivityZoneSensor(NestBinarySensor):
|
||||
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
self._state = self._device.has_ongoing_motion_in_zone(
|
||||
self.zone.zone_id)
|
||||
self._state = self.device.has_ongoing_motion_in_zone(self.zone.zone_id)
|
||||
|
||||
@@ -31,4 +31,4 @@ class TelldusLiveSensor(TelldusLiveEntity, BinarySensorDevice):
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if switch is on."""
|
||||
return self._device.is_on
|
||||
return self.device.is_on
|
||||
|
||||
@@ -46,7 +46,7 @@ class NestCamera(Camera):
|
||||
"""Initialize a Nest Camera."""
|
||||
super(NestCamera, self).__init__()
|
||||
self.structure = structure
|
||||
self._device = device
|
||||
self.device = device
|
||||
self._location = None
|
||||
self._name = None
|
||||
self._online = None
|
||||
@@ -93,7 +93,7 @@ class NestCamera(Camera):
|
||||
# Calling Nest API in is_streaming setter.
|
||||
# device.is_streaming would not immediately change until the process
|
||||
# finished in Nest Cam.
|
||||
self._device.is_streaming = False
|
||||
self.device.is_streaming = False
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn on camera."""
|
||||
@@ -105,15 +105,15 @@ class NestCamera(Camera):
|
||||
# Calling Nest API in is_streaming setter.
|
||||
# device.is_streaming would not immediately change until the process
|
||||
# finished in Nest Cam.
|
||||
self._device.is_streaming = True
|
||||
self.device.is_streaming = True
|
||||
|
||||
def update(self):
|
||||
"""Cache value from Python-nest."""
|
||||
self._location = self._device.where
|
||||
self._name = self._device.name
|
||||
self._online = self._device.online
|
||||
self._is_streaming = self._device.is_streaming
|
||||
self._is_video_history_enabled = self._device.is_video_history_enabled
|
||||
self._location = self.device.where
|
||||
self._name = self.device.name
|
||||
self._online = self.device.online
|
||||
self._is_streaming = self.device.is_streaming
|
||||
self._is_video_history_enabled = self.device.is_video_history_enabled
|
||||
|
||||
if self._is_video_history_enabled:
|
||||
# NestAware allowed 10/min
|
||||
@@ -130,7 +130,7 @@ class NestCamera(Camera):
|
||||
"""Return a still image response from the camera."""
|
||||
now = utcnow()
|
||||
if self._ready_for_snapshot(now):
|
||||
url = self._device.snapshot_url
|
||||
url = self.device.snapshot_url
|
||||
|
||||
try:
|
||||
response = requests.get(url)
|
||||
|
||||
@@ -64,7 +64,10 @@ def _resize_image(image, opts):
|
||||
quality = opts.quality or DEFAULT_QUALITY
|
||||
new_width = opts.max_width
|
||||
|
||||
img = Image.open(io.BytesIO(image))
|
||||
try:
|
||||
img = Image.open(io.BytesIO(image))
|
||||
except IOError:
|
||||
return image
|
||||
imgfmt = str(img.format)
|
||||
if imgfmt not in ('PNG', 'JPEG'):
|
||||
_LOGGER.debug("Image is of unsupported type: %s", imgfmt)
|
||||
|
||||
@@ -118,7 +118,7 @@ class KNXClimate(ClimateDevice):
|
||||
|
||||
def __init__(self, hass, device):
|
||||
"""Initialize of a KNX climate device."""
|
||||
self._device = device
|
||||
self.device = device
|
||||
self.hass = hass
|
||||
self.async_register_callbacks()
|
||||
|
||||
@@ -126,7 +126,7 @@ class KNXClimate(ClimateDevice):
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
support = SUPPORT_TARGET_TEMPERATURE
|
||||
if self._device.supports_operation_mode:
|
||||
if self.device.supports_operation_mode:
|
||||
support |= SUPPORT_OPERATION_MODE
|
||||
return support
|
||||
|
||||
@@ -135,12 +135,12 @@ class KNXClimate(ClimateDevice):
|
||||
async def after_update_callback(device):
|
||||
"""Call after device was updated."""
|
||||
await self.async_update_ha_state()
|
||||
self._device.register_device_updated_cb(after_update_callback)
|
||||
self.device.register_device_updated_cb(after_update_callback)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the KNX device."""
|
||||
return self._device.name
|
||||
return self.device.name
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
@@ -160,41 +160,41 @@ class KNXClimate(ClimateDevice):
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._device.temperature.value
|
||||
return self.device.temperature.value
|
||||
|
||||
@property
|
||||
def target_temperature_step(self):
|
||||
"""Return the supported step of target temperature."""
|
||||
return self._device.setpoint_shift_step
|
||||
return self.device.setpoint_shift_step
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._device.target_temperature.value
|
||||
return self.device.target_temperature.value
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return self._device.target_temperature_min
|
||||
return self.device.target_temperature_min
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return self._device.target_temperature_max
|
||||
return self.device.target_temperature_max
|
||||
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
await self._device.set_target_temperature(temperature)
|
||||
await self.device.set_target_temperature(temperature)
|
||||
await self.async_update_ha_state()
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if self._device.supports_operation_mode:
|
||||
return self._device.operation_mode.value
|
||||
if self.device.supports_operation_mode:
|
||||
return self.device.operation_mode.value
|
||||
return None
|
||||
|
||||
@property
|
||||
@@ -202,11 +202,11 @@ class KNXClimate(ClimateDevice):
|
||||
"""Return the list of available operation modes."""
|
||||
return [operation_mode.value for
|
||||
operation_mode in
|
||||
self._device.get_supported_operation_modes()]
|
||||
self.device.get_supported_operation_modes()]
|
||||
|
||||
async def async_set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
if self._device.supports_operation_mode:
|
||||
if self.device.supports_operation_mode:
|
||||
from xknx.knx import HVACOperationMode
|
||||
knx_operation_mode = HVACOperationMode(operation_mode)
|
||||
await self._device.set_operation_mode(knx_operation_mode)
|
||||
await self.device.set_operation_mode(knx_operation_mode)
|
||||
|
||||
@@ -57,7 +57,7 @@ class NestThermostat(ClimateDevice):
|
||||
"""Initialize the thermostat."""
|
||||
self._unit = temp_unit
|
||||
self.structure = structure
|
||||
self._device = device
|
||||
self.device = device
|
||||
self._fan_list = [STATE_ON, STATE_AUTO]
|
||||
|
||||
# Set the default supported features
|
||||
@@ -68,13 +68,13 @@ class NestThermostat(ClimateDevice):
|
||||
self._operation_list = [STATE_OFF]
|
||||
|
||||
# Add supported nest thermostat features
|
||||
if self._device.can_heat:
|
||||
if self.device.can_heat:
|
||||
self._operation_list.append(STATE_HEAT)
|
||||
|
||||
if self._device.can_cool:
|
||||
if self.device.can_cool:
|
||||
self._operation_list.append(STATE_COOL)
|
||||
|
||||
if self._device.can_heat and self._device.can_cool:
|
||||
if self.device.can_heat and self.device.can_cool:
|
||||
self._operation_list.append(STATE_AUTO)
|
||||
self._support_flags = (self._support_flags |
|
||||
SUPPORT_TARGET_TEMPERATURE_HIGH |
|
||||
@@ -83,7 +83,7 @@ class NestThermostat(ClimateDevice):
|
||||
self._operation_list.append(STATE_ECO)
|
||||
|
||||
# feature of device
|
||||
self._has_fan = self._device.has_fan
|
||||
self._has_fan = self.device.has_fan
|
||||
if self._has_fan:
|
||||
self._support_flags = (self._support_flags | SUPPORT_FAN_MODE)
|
||||
|
||||
@@ -125,7 +125,7 @@ class NestThermostat(ClimateDevice):
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return unique ID for this device."""
|
||||
return self._device.serial
|
||||
return self.device.serial
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -202,7 +202,7 @@ class NestThermostat(ClimateDevice):
|
||||
_LOGGER.debug("Nest set_temperature-output-value=%s", temp)
|
||||
try:
|
||||
if temp is not None:
|
||||
self._device.target = temp
|
||||
self.device.target = temp
|
||||
except nest.nest.APIError as api_error:
|
||||
_LOGGER.error("An error occurred while setting temperature: %s",
|
||||
api_error)
|
||||
@@ -220,7 +220,7 @@ class NestThermostat(ClimateDevice):
|
||||
_LOGGER.error(
|
||||
"An error occurred while setting device mode. "
|
||||
"Invalid operation mode: %s", operation_mode)
|
||||
self._device.mode = device_mode
|
||||
self.device.mode = device_mode
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
@@ -254,7 +254,7 @@ class NestThermostat(ClimateDevice):
|
||||
def set_fan_mode(self, fan_mode):
|
||||
"""Turn fan on/off."""
|
||||
if self._has_fan:
|
||||
self._device.fan = fan_mode.lower()
|
||||
self.device.fan = fan_mode.lower()
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
@@ -268,20 +268,20 @@ class NestThermostat(ClimateDevice):
|
||||
|
||||
def update(self):
|
||||
"""Cache value from Python-nest."""
|
||||
self._location = self._device.where
|
||||
self._name = self._device.name
|
||||
self._humidity = self._device.humidity
|
||||
self._temperature = self._device.temperature
|
||||
self._mode = self._device.mode
|
||||
self._target_temperature = self._device.target
|
||||
self._fan = self._device.fan
|
||||
self._location = self.device.where
|
||||
self._name = self.device.name
|
||||
self._humidity = self.device.humidity
|
||||
self._temperature = self.device.temperature
|
||||
self._mode = self.device.mode
|
||||
self._target_temperature = self.device.target
|
||||
self._fan = self.device.fan
|
||||
self._away = self.structure.away == 'away'
|
||||
self._eco_temperature = self._device.eco_temperature
|
||||
self._locked_temperature = self._device.locked_temperature
|
||||
self._min_temperature = self._device.min_temperature
|
||||
self._max_temperature = self._device.max_temperature
|
||||
self._is_locked = self._device.is_locked
|
||||
if self._device.temperature_scale == 'C':
|
||||
self._eco_temperature = self.device.eco_temperature
|
||||
self._locked_temperature = self.device.locked_temperature
|
||||
self._min_temperature = self.device.min_temperature
|
||||
self._max_temperature = self.device.max_temperature
|
||||
self._is_locked = self.device.is_locked
|
||||
if self.device.temperature_scale == 'C':
|
||||
self._temperature_scale = TEMP_CELSIUS
|
||||
else:
|
||||
self._temperature_scale = TEMP_FAHRENHEIT
|
||||
|
||||
@@ -120,7 +120,7 @@ class RadioThermostat(ClimateDevice):
|
||||
|
||||
def __init__(self, device, hold_temp, away_temps):
|
||||
"""Initialize the thermostat."""
|
||||
self._device = device
|
||||
self.device = device
|
||||
self._target_temperature = None
|
||||
self._current_temperature = None
|
||||
self._current_operation = STATE_IDLE
|
||||
@@ -138,7 +138,7 @@ class RadioThermostat(ClimateDevice):
|
||||
# Fan circulate mode is only supported by the CT80 models.
|
||||
import radiotherm
|
||||
self._is_model_ct80 = isinstance(
|
||||
self._device, radiotherm.thermostat.CT80)
|
||||
self.device, radiotherm.thermostat.CT80)
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
@@ -194,7 +194,7 @@ class RadioThermostat(ClimateDevice):
|
||||
"""Turn fan on/off."""
|
||||
code = FAN_MODE_TO_CODE.get(fan_mode, None)
|
||||
if code is not None:
|
||||
self._device.fmode = code
|
||||
self.device.fmode = code
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
@@ -234,15 +234,15 @@ class RadioThermostat(ClimateDevice):
|
||||
# First time - get the name from the thermostat. This is
|
||||
# normally set in the radio thermostat web app.
|
||||
if self._name is None:
|
||||
self._name = self._device.name['raw']
|
||||
self._name = self.device.name['raw']
|
||||
|
||||
# Request the current state from the thermostat.
|
||||
data = self._device.tstat['raw']
|
||||
data = self.device.tstat['raw']
|
||||
|
||||
current_temp = data['temp']
|
||||
if current_temp == -1:
|
||||
_LOGGER.error('%s (%s) was busy (temp == -1)', self._name,
|
||||
self._device.host)
|
||||
self.device.host)
|
||||
return
|
||||
|
||||
# Map thermostat values into various STATE_ flags.
|
||||
@@ -277,30 +277,30 @@ class RadioThermostat(ClimateDevice):
|
||||
temperature = round_temp(temperature)
|
||||
|
||||
if self._current_operation == STATE_COOL:
|
||||
self._device.t_cool = temperature
|
||||
self.device.t_cool = temperature
|
||||
elif self._current_operation == STATE_HEAT:
|
||||
self._device.t_heat = temperature
|
||||
self.device.t_heat = temperature
|
||||
elif self._current_operation == STATE_AUTO:
|
||||
if self._tstate == STATE_COOL:
|
||||
self._device.t_cool = temperature
|
||||
self.device.t_cool = temperature
|
||||
elif self._tstate == STATE_HEAT:
|
||||
self._device.t_heat = temperature
|
||||
self.device.t_heat = temperature
|
||||
|
||||
# Only change the hold if requested or if hold mode was turned
|
||||
# on and we haven't set it yet.
|
||||
if kwargs.get('hold_changed', False) or not self._hold_set:
|
||||
if self._hold_temp or self._away:
|
||||
self._device.hold = 1
|
||||
self.device.hold = 1
|
||||
self._hold_set = True
|
||||
else:
|
||||
self._device.hold = 0
|
||||
self.device.hold = 0
|
||||
|
||||
def set_time(self):
|
||||
"""Set device time."""
|
||||
# Calling this clears any local temperature override and
|
||||
# reverts to the scheduled temperature.
|
||||
now = datetime.datetime.now()
|
||||
self._device.time = {
|
||||
self.device.time = {
|
||||
'day': now.weekday(),
|
||||
'hour': now.hour,
|
||||
'minute': now.minute
|
||||
@@ -309,13 +309,13 @@ class RadioThermostat(ClimateDevice):
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode (auto, cool, heat, off)."""
|
||||
if operation_mode in (STATE_OFF, STATE_AUTO):
|
||||
self._device.tmode = TEMP_MODE_TO_CODE[operation_mode]
|
||||
self.device.tmode = TEMP_MODE_TO_CODE[operation_mode]
|
||||
|
||||
# Setting t_cool or t_heat automatically changes tmode.
|
||||
elif operation_mode == STATE_COOL:
|
||||
self._device.t_cool = self._target_temperature
|
||||
self.device.t_cool = self._target_temperature
|
||||
elif operation_mode == STATE_HEAT:
|
||||
self._device.t_heat = self._target_temperature
|
||||
self.device.t_heat = self._target_temperature
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away on.
|
||||
|
||||
@@ -96,7 +96,7 @@ class KNXCover(CoverDevice):
|
||||
|
||||
def __init__(self, hass, device):
|
||||
"""Initialize the cover."""
|
||||
self._device = device
|
||||
self.device = device
|
||||
self.hass = hass
|
||||
self.async_register_callbacks()
|
||||
|
||||
@@ -108,12 +108,12 @@ class KNXCover(CoverDevice):
|
||||
async def after_update_callback(device):
|
||||
"""Call after device was updated."""
|
||||
await self.async_update_ha_state()
|
||||
self._device.register_device_updated_cb(after_update_callback)
|
||||
self.device.register_device_updated_cb(after_update_callback)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the KNX device."""
|
||||
return self._device.name
|
||||
return self.device.name
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
@@ -130,56 +130,56 @@ class KNXCover(CoverDevice):
|
||||
"""Flag supported features."""
|
||||
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | \
|
||||
SUPPORT_SET_POSITION | SUPPORT_STOP
|
||||
if self._device.supports_angle:
|
||||
if self.device.supports_angle:
|
||||
supported_features |= SUPPORT_SET_TILT_POSITION
|
||||
return supported_features
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return the current position of the cover."""
|
||||
return self._device.current_position()
|
||||
return self.device.current_position()
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
return self._device.is_closed()
|
||||
return self.device.is_closed()
|
||||
|
||||
async def async_close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
if not self._device.is_closed():
|
||||
await self._device.set_down()
|
||||
if not self.device.is_closed():
|
||||
await self.device.set_down()
|
||||
self.start_auto_updater()
|
||||
|
||||
async def async_open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
if not self._device.is_open():
|
||||
await self._device.set_up()
|
||||
if not self.device.is_open():
|
||||
await self.device.set_up()
|
||||
self.start_auto_updater()
|
||||
|
||||
async def async_set_cover_position(self, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
if ATTR_POSITION in kwargs:
|
||||
position = kwargs[ATTR_POSITION]
|
||||
await self._device.set_position(position)
|
||||
await self.device.set_position(position)
|
||||
self.start_auto_updater()
|
||||
|
||||
async def async_stop_cover(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
await self._device.stop()
|
||||
await self.device.stop()
|
||||
self.stop_auto_updater()
|
||||
|
||||
@property
|
||||
def current_cover_tilt_position(self):
|
||||
"""Return current tilt position of cover."""
|
||||
if not self._device.supports_angle:
|
||||
if not self.device.supports_angle:
|
||||
return None
|
||||
return self._device.current_angle()
|
||||
return self.device.current_angle()
|
||||
|
||||
async def async_set_cover_tilt_position(self, **kwargs):
|
||||
"""Move the cover tilt to a specific position."""
|
||||
if ATTR_TILT_POSITION in kwargs:
|
||||
tilt_position = kwargs[ATTR_TILT_POSITION]
|
||||
await self._device.set_angle(tilt_position)
|
||||
await self.device.set_angle(tilt_position)
|
||||
|
||||
def start_auto_updater(self):
|
||||
"""Start the autoupdater to update HASS while cover is moving."""
|
||||
@@ -197,7 +197,7 @@ class KNXCover(CoverDevice):
|
||||
def auto_updater_hook(self, now):
|
||||
"""Call for the autoupdater."""
|
||||
self.async_schedule_update_ha_state()
|
||||
if self._device.position_reached():
|
||||
if self.device.position_reached():
|
||||
self.stop_auto_updater()
|
||||
|
||||
self.hass.add_job(self._device.auto_stop_if_necessary())
|
||||
self.hass.add_job(self.device.auto_stop_if_necessary())
|
||||
|
||||
@@ -28,19 +28,19 @@ class TelldusLiveCover(TelldusLiveEntity, CoverDevice):
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return the current position of the cover."""
|
||||
return self._device.is_down
|
||||
return self.device.is_down
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
self._device.down()
|
||||
self.device.down()
|
||||
self.changed()
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
self._device.up()
|
||||
self.device.up()
|
||||
self.changed()
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
self._device.stop()
|
||||
self.device.stop()
|
||||
self.changed()
|
||||
|
||||
@@ -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==20180825.0']
|
||||
REQUIREMENTS = ['home-assistant-frontend==20180826.0']
|
||||
|
||||
DOMAIN = 'frontend'
|
||||
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log',
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"error": {
|
||||
"invalid_2fa": "Invalid 2 Factor Authorization, please try again.",
|
||||
"invalid_2fa_method": "Invalig 2FA Method (Verify on Phone).",
|
||||
"invalid_2fa_method": "Invalid 2FA Method (Verify on Phone).",
|
||||
"invalid_login": "Invalid Login, please try again."
|
||||
},
|
||||
"step": {
|
||||
|
||||
5
homeassistant/components/hangouts/.translations/it.json
Normal file
5
homeassistant/components/hangouts/.translations/it.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"config": {
|
||||
"title": "Google Hangouts"
|
||||
}
|
||||
}
|
||||
13
homeassistant/components/hangouts/.translations/no.json
Normal file
13
homeassistant/components/hangouts/.translations/no.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"email": "E-postadresse",
|
||||
"password": "Passord"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Google Hangouts"
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
"email": "Adres e-mail",
|
||||
"password": "Has\u0142o"
|
||||
},
|
||||
"title": "Login Google Hangouts"
|
||||
"title": "Logowanie do Google Hangouts"
|
||||
}
|
||||
},
|
||||
"title": "Google Hangouts"
|
||||
|
||||
28
homeassistant/components/hangouts/.translations/pt-BR.json
Normal file
28
homeassistant/components/hangouts/.translations/pt-BR.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Hangouts do Google j\u00e1 est\u00e1 configurado.",
|
||||
"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."
|
||||
},
|
||||
"step": {
|
||||
"2fa": {
|
||||
"data": {
|
||||
"2fa": "Pin 2FA"
|
||||
},
|
||||
"title": ""
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"email": "Endere\u00e7o de e-mail",
|
||||
"password": "Senha"
|
||||
},
|
||||
"title": "Login do Hangouts do Google"
|
||||
}
|
||||
},
|
||||
"title": "Hangouts do Google"
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,15 @@
|
||||
"unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430"
|
||||
},
|
||||
"error": {
|
||||
"invalid_2fa": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.",
|
||||
"invalid_2fa_method": "\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 \u0441\u043f\u043e\u0441\u043e\u0431 \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 (\u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043d\u0430 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0435).",
|
||||
"invalid_login": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430."
|
||||
},
|
||||
"step": {
|
||||
"2fa": {
|
||||
"data": {
|
||||
"2fa": "\u041f\u0438\u043d-\u043a\u043e\u0434 \u0434\u043b\u044f \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"
|
||||
},
|
||||
"title": "\u0414\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f"
|
||||
},
|
||||
"user": {
|
||||
|
||||
31
homeassistant/components/hangouts/.translations/zh-Hant.json
Normal file
31
homeassistant/components/hangouts/.translations/zh-Hant.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Google Hangouts \u5df2\u7d93\u8a2d\u5b9a",
|
||||
"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_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"
|
||||
},
|
||||
"step": {
|
||||
"2fa": {
|
||||
"data": {
|
||||
"2fa": "\u8a8d\u8b49\u78bc"
|
||||
},
|
||||
"description": "\u7a7a\u767d",
|
||||
"title": "\u5169\u968e\u6bb5\u8a8d\u8b49"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"email": "\u96fb\u5b50\u90f5\u4ef6",
|
||||
"password": "\u5bc6\u78bc"
|
||||
},
|
||||
"description": "\u7a7a\u767d",
|
||||
"title": "\u767b\u5165 Google Hangouts"
|
||||
}
|
||||
},
|
||||
"title": "Google Hangouts"
|
||||
}
|
||||
}
|
||||
@@ -195,23 +195,15 @@ class HangoutsBot:
|
||||
import hangups
|
||||
self._user_list, self._conversation_list = \
|
||||
(await hangups.build_user_conversation_list(self._client))
|
||||
users = {}
|
||||
conversations = {}
|
||||
for user in self._user_list.get_all():
|
||||
users[str(user.id_.chat_id)] = {'full_name': user.full_name,
|
||||
'is_self': user.is_self}
|
||||
|
||||
for conv in self._conversation_list.get_all():
|
||||
users_in_conversation = {}
|
||||
for i, conv in enumerate(self._conversation_list.get_all()):
|
||||
users_in_conversation = []
|
||||
for user in conv.users:
|
||||
users_in_conversation[str(user.id_.chat_id)] = \
|
||||
{'full_name': user.full_name, 'is_self': user.is_self}
|
||||
conversations[str(conv.id_)] = \
|
||||
{'name': conv.name, 'users': users_in_conversation}
|
||||
users_in_conversation.append(user.full_name)
|
||||
conversations[str(i)] = {'id': str(conv.id_),
|
||||
'name': conv.name,
|
||||
'users': users_in_conversation}
|
||||
|
||||
self.hass.states.async_set("{}.users".format(DOMAIN),
|
||||
len(self._user_list.get_all()),
|
||||
attributes=users)
|
||||
self.hass.states.async_set("{}.conversations".format(DOMAIN),
|
||||
len(self._conversation_list.get_all()),
|
||||
attributes=conversations)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"pin": "Codice Pin (opzionale)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
"abort": {
|
||||
"already_configured": "O Accesspoint j\u00e1 est\u00e1 configurado",
|
||||
"conection_aborted": "N\u00e3o foi poss\u00edvel conectar ao servidor HMIP",
|
||||
"connection_aborted": "N\u00e3o foi poss\u00edvel conectar ao servidor HMIP",
|
||||
"unknown": "Ocorreu um erro desconhecido."
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"abort": {
|
||||
"already_configured": "Accesspoint \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
|
||||
"conection_aborted": "\u7121\u6cd5\u9023\u7dda\u81f3 HMIP \u4f3a\u670d\u5668",
|
||||
"connection_aborted": "\u7121\u6cd5\u9023\u7dda\u81f3 HMIP \u4f3a\u670d\u5668",
|
||||
"unknown": "\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002"
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -24,6 +24,6 @@
|
||||
"title": "Hub de links"
|
||||
}
|
||||
},
|
||||
"title": "Philips Hue"
|
||||
"title": ""
|
||||
}
|
||||
}
|
||||
@@ -46,29 +46,29 @@ class JuicenetDevice(Entity):
|
||||
def __init__(self, device, sensor_type, hass):
|
||||
"""Initialise the sensor."""
|
||||
self.hass = hass
|
||||
self._device = device
|
||||
self.device = device
|
||||
self.type = sensor_type
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._device.name()
|
||||
return self.device.name()
|
||||
|
||||
def update(self):
|
||||
"""Update state of the device."""
|
||||
self._device.update_state()
|
||||
self.device.update_state()
|
||||
|
||||
@property
|
||||
def _manufacturer_device_id(self):
|
||||
"""Return the manufacturer device id."""
|
||||
return self._device.id()
|
||||
return self.device.id()
|
||||
|
||||
@property
|
||||
def _token(self):
|
||||
"""Return the device API token."""
|
||||
return self._device.token()
|
||||
return self.device.token()
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID."""
|
||||
return "{}-{}".format(self._device.id(), self.type)
|
||||
return "{}-{}".format(self.device.id(), self.type)
|
||||
|
||||
@@ -79,7 +79,7 @@ class KNXLight(Light):
|
||||
|
||||
def __init__(self, hass, device):
|
||||
"""Initialize of KNX light."""
|
||||
self._device = device
|
||||
self.device = device
|
||||
self.hass = hass
|
||||
self.async_register_callbacks()
|
||||
|
||||
@@ -89,12 +89,12 @@ class KNXLight(Light):
|
||||
async def after_update_callback(device):
|
||||
"""Call after device was updated."""
|
||||
await self.async_update_ha_state()
|
||||
self._device.register_device_updated_cb(after_update_callback)
|
||||
self.device.register_device_updated_cb(after_update_callback)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the KNX device."""
|
||||
return self._device.name
|
||||
return self.device.name
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
@@ -109,15 +109,15 @@ class KNXLight(Light):
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
return self._device.current_brightness \
|
||||
if self._device.supports_brightness else \
|
||||
return self.device.current_brightness \
|
||||
if self.device.supports_brightness else \
|
||||
None
|
||||
|
||||
@property
|
||||
def hs_color(self):
|
||||
"""Return the HS color value."""
|
||||
if self._device.supports_color:
|
||||
return color_util.color_RGB_to_hs(*self._device.current_color)
|
||||
if self.device.supports_color:
|
||||
return color_util.color_RGB_to_hs(*self.device.current_color)
|
||||
return None
|
||||
|
||||
@property
|
||||
@@ -143,30 +143,30 @@ class KNXLight(Light):
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if light is on."""
|
||||
return self._device.state
|
||||
return self.device.state
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
flags = 0
|
||||
if self._device.supports_brightness:
|
||||
if self.device.supports_brightness:
|
||||
flags |= SUPPORT_BRIGHTNESS
|
||||
if self._device.supports_color:
|
||||
if self.device.supports_color:
|
||||
flags |= SUPPORT_COLOR
|
||||
return flags
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn the light on."""
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
if self._device.supports_brightness:
|
||||
await self._device.set_brightness(int(kwargs[ATTR_BRIGHTNESS]))
|
||||
if self.device.supports_brightness:
|
||||
await self.device.set_brightness(int(kwargs[ATTR_BRIGHTNESS]))
|
||||
elif ATTR_HS_COLOR in kwargs:
|
||||
if self._device.supports_color:
|
||||
await self._device.set_color(color_util.color_hs_to_RGB(
|
||||
if self.device.supports_color:
|
||||
await self.device.set_color(color_util.color_hs_to_RGB(
|
||||
*kwargs[ATTR_HS_COLOR]))
|
||||
else:
|
||||
await self._device.set_on()
|
||||
await self.device.set_on()
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Turn the light off."""
|
||||
await self._device.set_off()
|
||||
await self.device.set_off()
|
||||
|
||||
@@ -27,9 +27,9 @@ class QSLight(QSToggleEntity, Light):
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light (0-255)."""
|
||||
return self._device.value if self._device.is_dimmer else None
|
||||
return self.device.value if self.device.is_dimmer else None
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_BRIGHTNESS if self._device.is_dimmer else 0
|
||||
return SUPPORT_BRIGHTNESS if self.device.is_dimmer else 0
|
||||
|
||||
@@ -38,7 +38,7 @@ class TelldusLiveLight(TelldusLiveEntity, Light):
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
return self._device.dim_level
|
||||
return self.device.dim_level
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
@@ -48,15 +48,15 @@ class TelldusLiveLight(TelldusLiveEntity, Light):
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if light is on."""
|
||||
return self._device.is_on
|
||||
return self.device.is_on
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the light on."""
|
||||
brightness = kwargs.get(ATTR_BRIGHTNESS, self._last_brightness)
|
||||
self._device.dim(level=brightness)
|
||||
self.device.dim(level=brightness)
|
||||
self.changed()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the light off."""
|
||||
self._device.turn_off()
|
||||
self.device.turn_off()
|
||||
self.changed()
|
||||
|
||||
@@ -133,7 +133,7 @@ class EmbyDevice(MediaPlayerDevice):
|
||||
_LOGGER.debug("New Emby Device initialized with ID: %s", device_id)
|
||||
self.emby = emby
|
||||
self.device_id = device_id
|
||||
self._device = self.emby.devices[self.device_id]
|
||||
self.device = self.emby.devices[self.device_id]
|
||||
|
||||
self._hidden = False
|
||||
self._available = True
|
||||
@@ -151,11 +151,11 @@ class EmbyDevice(MediaPlayerDevice):
|
||||
def async_update_callback(self, msg):
|
||||
"""Handle device updates."""
|
||||
# Check if we should update progress
|
||||
if self._device.media_position:
|
||||
if self._device.media_position != self.media_status_last_position:
|
||||
self.media_status_last_position = self._device.media_position
|
||||
if self.device.media_position:
|
||||
if self.device.media_position != self.media_status_last_position:
|
||||
self.media_status_last_position = self.device.media_position
|
||||
self.media_status_received = dt_util.utcnow()
|
||||
elif not self._device.is_nowplaying:
|
||||
elif not self.device.is_nowplaying:
|
||||
# No position, but we have an old value and are still playing
|
||||
self.media_status_last_position = None
|
||||
self.media_status_received = None
|
||||
@@ -188,12 +188,12 @@ class EmbyDevice(MediaPlayerDevice):
|
||||
@property
|
||||
def supports_remote_control(self):
|
||||
"""Return control ability."""
|
||||
return self._device.supports_remote_control
|
||||
return self.device.supports_remote_control
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return ('Emby - {} - {}'.format(self._device.client, self._device.name)
|
||||
return ('Emby - {} - {}'.format(self.device.client, self.device.name)
|
||||
or DEVICE_DEFAULT_NAME)
|
||||
|
||||
@property
|
||||
@@ -204,7 +204,7 @@ class EmbyDevice(MediaPlayerDevice):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
state = self._device.state
|
||||
state = self.device.state
|
||||
if state == 'Paused':
|
||||
return STATE_PAUSED
|
||||
if state == 'Playing':
|
||||
@@ -218,17 +218,17 @@ class EmbyDevice(MediaPlayerDevice):
|
||||
def app_name(self):
|
||||
"""Return current user as app_name."""
|
||||
# Ideally the media_player object would have a user property.
|
||||
return self._device.username
|
||||
return self.device.username
|
||||
|
||||
@property
|
||||
def media_content_id(self):
|
||||
"""Content ID of current playing media."""
|
||||
return self._device.media_id
|
||||
return self.device.media_id
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
"""Content type of current playing media."""
|
||||
media_type = self._device.media_type
|
||||
media_type = self.device.media_type
|
||||
if media_type == 'Episode':
|
||||
return MEDIA_TYPE_TVSHOW
|
||||
if media_type == 'Movie':
|
||||
@@ -246,7 +246,7 @@ class EmbyDevice(MediaPlayerDevice):
|
||||
@property
|
||||
def media_duration(self):
|
||||
"""Return the duration of current playing media in seconds."""
|
||||
return self._device.media_runtime
|
||||
return self.device.media_runtime
|
||||
|
||||
@property
|
||||
def media_position(self):
|
||||
@@ -265,42 +265,42 @@ class EmbyDevice(MediaPlayerDevice):
|
||||
@property
|
||||
def media_image_url(self):
|
||||
"""Return the image URL of current playing media."""
|
||||
return self._device.media_image_url
|
||||
return self.device.media_image_url
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Return the title of current playing media."""
|
||||
return self._device.media_title
|
||||
return self.device.media_title
|
||||
|
||||
@property
|
||||
def media_season(self):
|
||||
"""Season of current playing media (TV Show only)."""
|
||||
return self._device.media_season
|
||||
return self.device.media_season
|
||||
|
||||
@property
|
||||
def media_series_title(self):
|
||||
"""Return the title of the series of current playing media (TV)."""
|
||||
return self._device.media_series_title
|
||||
return self.device.media_series_title
|
||||
|
||||
@property
|
||||
def media_episode(self):
|
||||
"""Return the episode of current playing media (TV only)."""
|
||||
return self._device.media_episode
|
||||
return self.device.media_episode
|
||||
|
||||
@property
|
||||
def media_album_name(self):
|
||||
"""Return the album name of current playing media (Music only)."""
|
||||
return self._device.media_album_name
|
||||
return self.device.media_album_name
|
||||
|
||||
@property
|
||||
def media_artist(self):
|
||||
"""Return the artist of current playing media (Music track only)."""
|
||||
return self._device.media_artist
|
||||
return self.device.media_artist
|
||||
|
||||
@property
|
||||
def media_album_artist(self):
|
||||
"""Return the album artist of current playing media (Music only)."""
|
||||
return self._device.media_album_artist
|
||||
return self.device.media_album_artist
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
@@ -314,39 +314,39 @@ class EmbyDevice(MediaPlayerDevice):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self._device.media_play()
|
||||
return self.device.media_play()
|
||||
|
||||
def async_media_pause(self):
|
||||
"""Pause the media player.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self._device.media_pause()
|
||||
return self.device.media_pause()
|
||||
|
||||
def async_media_stop(self):
|
||||
"""Stop the media player.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self._device.media_stop()
|
||||
return self.device.media_stop()
|
||||
|
||||
def async_media_next_track(self):
|
||||
"""Send next track command.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self._device.media_next()
|
||||
return self.device.media_next()
|
||||
|
||||
def async_media_previous_track(self):
|
||||
"""Send next track command.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self._device.media_previous()
|
||||
return self.device.media_previous()
|
||||
|
||||
def async_media_seek(self, position):
|
||||
"""Send seek command.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self._device.media_seek(position)
|
||||
return self.device.media_seek(position)
|
||||
|
||||
@@ -454,7 +454,7 @@ class PlexClient(MediaPlayerDevice):
|
||||
elif self._player_state == 'paused':
|
||||
self._is_player_active = True
|
||||
self._state = STATE_PAUSED
|
||||
elif self._device:
|
||||
elif self.device:
|
||||
self._is_player_active = False
|
||||
self._state = STATE_IDLE
|
||||
else:
|
||||
@@ -528,6 +528,11 @@ class PlexClient(MediaPlayerDevice):
|
||||
"""Return the library name of playing media."""
|
||||
return self._app_name
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
"""Return the device, if any."""
|
||||
return self.device
|
||||
|
||||
@property
|
||||
def marked_unavailable(self):
|
||||
"""Return time device was marked unavailable."""
|
||||
@@ -666,7 +671,7 @@ class PlexClient(MediaPlayerDevice):
|
||||
SUPPORT_TURN_OFF)
|
||||
# Not all devices support playback functionality
|
||||
# Playback includes volume, stop/play/pause, etc.
|
||||
if self._device and 'playback' in self._device_protocol_capabilities:
|
||||
if self.device and 'playback' in self._device_protocol_capabilities:
|
||||
return (SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK |
|
||||
SUPPORT_NEXT_TRACK | SUPPORT_STOP |
|
||||
SUPPORT_VOLUME_SET | SUPPORT_PLAY |
|
||||
@@ -676,22 +681,22 @@ class PlexClient(MediaPlayerDevice):
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
"""Set volume level, range 0..1."""
|
||||
if self._device and 'playback' in self._device_protocol_capabilities:
|
||||
self._device.setVolume(
|
||||
if self.device and 'playback' in self._device_protocol_capabilities:
|
||||
self.device.setVolume(
|
||||
int(volume * 100), self._active_media_plexapi_type)
|
||||
self._volume_level = volume # store since we can't retrieve
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
"""Return the volume level of the client (0..1)."""
|
||||
if (self._is_player_active and self._device and
|
||||
if (self._is_player_active and self.device and
|
||||
'playback' in self._device_protocol_capabilities):
|
||||
return self._volume_level
|
||||
|
||||
@property
|
||||
def is_volume_muted(self):
|
||||
"""Return boolean if volume is currently muted."""
|
||||
if self._is_player_active and self._device:
|
||||
if self._is_player_active and self.device:
|
||||
return self._volume_muted
|
||||
|
||||
def mute_volume(self, mute):
|
||||
@@ -701,7 +706,7 @@ class PlexClient(MediaPlayerDevice):
|
||||
- On mute, store volume and set volume to 0
|
||||
- On unmute, set volume to previously stored volume
|
||||
"""
|
||||
if not (self._device and
|
||||
if not (self.device and
|
||||
'playback' in self._device_protocol_capabilities):
|
||||
return
|
||||
|
||||
@@ -714,18 +719,18 @@ class PlexClient(MediaPlayerDevice):
|
||||
|
||||
def media_play(self):
|
||||
"""Send play command."""
|
||||
if self._device and 'playback' in self._device_protocol_capabilities:
|
||||
self._device.play(self._active_media_plexapi_type)
|
||||
if self.device and 'playback' in self._device_protocol_capabilities:
|
||||
self.device.play(self._active_media_plexapi_type)
|
||||
|
||||
def media_pause(self):
|
||||
"""Send pause command."""
|
||||
if self._device and 'playback' in self._device_protocol_capabilities:
|
||||
self._device.pause(self._active_media_plexapi_type)
|
||||
if self.device and 'playback' in self._device_protocol_capabilities:
|
||||
self.device.pause(self._active_media_plexapi_type)
|
||||
|
||||
def media_stop(self):
|
||||
"""Send stop command."""
|
||||
if self._device and 'playback' in self._device_protocol_capabilities:
|
||||
self._device.stop(self._active_media_plexapi_type)
|
||||
if self.device and 'playback' in self._device_protocol_capabilities:
|
||||
self.device.stop(self._active_media_plexapi_type)
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn the client off."""
|
||||
@@ -734,17 +739,17 @@ class PlexClient(MediaPlayerDevice):
|
||||
|
||||
def media_next_track(self):
|
||||
"""Send next track command."""
|
||||
if self._device and 'playback' in self._device_protocol_capabilities:
|
||||
self._device.skipNext(self._active_media_plexapi_type)
|
||||
if self.device and 'playback' in self._device_protocol_capabilities:
|
||||
self.device.skipNext(self._active_media_plexapi_type)
|
||||
|
||||
def media_previous_track(self):
|
||||
"""Send previous track command."""
|
||||
if self._device and 'playback' in self._device_protocol_capabilities:
|
||||
self._device.skipPrevious(self._active_media_plexapi_type)
|
||||
if self.device and 'playback' in self._device_protocol_capabilities:
|
||||
self.device.skipPrevious(self._active_media_plexapi_type)
|
||||
|
||||
def play_media(self, media_type, media_id, **kwargs):
|
||||
"""Play a piece of media."""
|
||||
if not (self._device and
|
||||
if not (self.device and
|
||||
'playback' in self._device_protocol_capabilities):
|
||||
return
|
||||
|
||||
@@ -752,7 +757,7 @@ class PlexClient(MediaPlayerDevice):
|
||||
|
||||
media = None
|
||||
if media_type == 'MUSIC':
|
||||
media = self._device.server.library.section(
|
||||
media = self.device.server.library.section(
|
||||
src['library_name']).get(src['artist_name']).album(
|
||||
src['album_name']).get(src['track_name'])
|
||||
elif media_type == 'EPISODE':
|
||||
@@ -760,9 +765,9 @@ class PlexClient(MediaPlayerDevice):
|
||||
src['library_name'], src['show_name'],
|
||||
src['season_number'], src['episode_number'])
|
||||
elif media_type == 'PLAYLIST':
|
||||
media = self._device.server.playlist(src['playlist_name'])
|
||||
media = self.device.server.playlist(src['playlist_name'])
|
||||
elif media_type == 'VIDEO':
|
||||
media = self._device.server.library.section(
|
||||
media = self.device.server.library.section(
|
||||
src['library_name']).get(src['video_name'])
|
||||
|
||||
import plexapi.playlist
|
||||
@@ -780,13 +785,13 @@ class PlexClient(MediaPlayerDevice):
|
||||
target_season = None
|
||||
target_episode = None
|
||||
|
||||
show = self._device.server.library.section(library_name).get(
|
||||
show = self.device.server.library.section(library_name).get(
|
||||
show_name)
|
||||
|
||||
if not season_number:
|
||||
playlist_name = "{} - {} Episodes".format(
|
||||
self.entity_id, show_name)
|
||||
return self._device.server.createPlaylist(
|
||||
return self.device.server.createPlaylist(
|
||||
playlist_name, show.episodes())
|
||||
|
||||
for season in show.seasons():
|
||||
@@ -803,7 +808,7 @@ class PlexClient(MediaPlayerDevice):
|
||||
if not episode_number:
|
||||
playlist_name = "{} - {} Season {} Episodes".format(
|
||||
self.entity_id, show_name, str(season_number))
|
||||
return self._device.server.createPlaylist(
|
||||
return self.device.server.createPlaylist(
|
||||
playlist_name, target_season.episodes())
|
||||
|
||||
for episode in target_season.episodes():
|
||||
@@ -821,22 +826,22 @@ class PlexClient(MediaPlayerDevice):
|
||||
|
||||
def _client_play_media(self, media, delete=False, **params):
|
||||
"""Instruct Plex client to play a piece of media."""
|
||||
if not (self._device and
|
||||
if not (self.device and
|
||||
'playback' in self._device_protocol_capabilities):
|
||||
_LOGGER.error("Client cannot play media: %s", self.entity_id)
|
||||
return
|
||||
|
||||
import plexapi.playqueue
|
||||
playqueue = plexapi.playqueue.PlayQueue.create(
|
||||
self._device.server, media, **params)
|
||||
self.device.server, media, **params)
|
||||
|
||||
# Delete dynamic playlists used to build playqueue (ex. play tv season)
|
||||
if delete:
|
||||
media.delete()
|
||||
|
||||
server_url = self._device.server.baseurl.split(':')
|
||||
self._device.sendCommand('playback/playMedia', **dict({
|
||||
'machineIdentifier': self._device.server.machineIdentifier,
|
||||
server_url = self.device.server.baseurl.split(':')
|
||||
self.device.sendCommand('playback/playMedia', **dict({
|
||||
'machineIdentifier': self.device.server.machineIdentifier,
|
||||
'address': server_url[1].strip('/'),
|
||||
'port': server_url[-1],
|
||||
'key': media.key,
|
||||
|
||||
@@ -323,8 +323,8 @@ class SoundTouchDevice(MediaPlayerDevice):
|
||||
_LOGGER.warning("Unable to create zone without slaves")
|
||||
else:
|
||||
_LOGGER.info("Creating zone with master %s",
|
||||
self.device.config.name)
|
||||
self.device.create_zone([slave.device for slave in slaves])
|
||||
self._device.config.name)
|
||||
self._device.create_zone([slave.device for slave in slaves])
|
||||
|
||||
def remove_zone_slave(self, slaves):
|
||||
"""
|
||||
@@ -341,8 +341,8 @@ class SoundTouchDevice(MediaPlayerDevice):
|
||||
_LOGGER.warning("Unable to find slaves to remove")
|
||||
else:
|
||||
_LOGGER.info("Removing slaves from zone with master %s",
|
||||
self.device.config.name)
|
||||
self.device.remove_zone_slave([slave.device for slave in slaves])
|
||||
self._device.config.name)
|
||||
self._device.remove_zone_slave([slave.device for slave in slaves])
|
||||
|
||||
def add_zone_slave(self, slaves):
|
||||
"""
|
||||
@@ -357,5 +357,5 @@ class SoundTouchDevice(MediaPlayerDevice):
|
||||
_LOGGER.warning("Unable to find slaves to add")
|
||||
else:
|
||||
_LOGGER.info("Adding slaves to zone with master %s",
|
||||
self.device.config.name)
|
||||
self.device.add_zone_slave([slave.device for slave in slaves])
|
||||
self._device.config.name)
|
||||
self._device.add_zone_slave([slave.device for slave in slaves])
|
||||
|
||||
@@ -282,12 +282,12 @@ class NestSensorDevice(Entity):
|
||||
|
||||
if device is not None:
|
||||
# device specific
|
||||
self._device = device
|
||||
self._name = "{} {}".format(self._device.name_long,
|
||||
self.device = device
|
||||
self._name = "{} {}".format(self.device.name_long,
|
||||
self.variable.replace('_', ' '))
|
||||
else:
|
||||
# structure only
|
||||
self._device = structure
|
||||
self.device = structure
|
||||
self._name = "{} {}".format(self.structure.name,
|
||||
self.variable.replace('_', ' '))
|
||||
|
||||
|
||||
@@ -61,13 +61,13 @@ class KNXNotificationService(BaseNotificationService):
|
||||
|
||||
def __init__(self, devices):
|
||||
"""Initialize the service."""
|
||||
self._devices = devices
|
||||
self.devices = devices
|
||||
|
||||
@property
|
||||
def targets(self):
|
||||
"""Return a dictionary of registered targets."""
|
||||
ret = {}
|
||||
for device in self._devices:
|
||||
for device in self.devices:
|
||||
ret[device.name] = device.name
|
||||
return ret
|
||||
|
||||
@@ -80,11 +80,11 @@ class KNXNotificationService(BaseNotificationService):
|
||||
|
||||
async def _async_send_to_all_devices(self, message):
|
||||
"""Send a notification to knx bus to all connected devices."""
|
||||
for device in self._devices:
|
||||
for device in self.devices:
|
||||
await device.set(message)
|
||||
|
||||
async def _async_send_to_device(self, message, names):
|
||||
"""Send a notification to knx bus to device with given names."""
|
||||
for device in self._devices:
|
||||
for device in self.devices:
|
||||
if device.name in names:
|
||||
await device.set(message)
|
||||
|
||||
@@ -98,13 +98,13 @@ class QSToggleEntity(QSEntity):
|
||||
|
||||
def __init__(self, qsid, qsusb):
|
||||
"""Initialize the ToggleEntity."""
|
||||
self._device = qsusb.devices[qsid]
|
||||
super().__init__(qsid, self._device.name)
|
||||
self.device = qsusb.devices[qsid]
|
||||
super().__init__(qsid, self.device.name)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Check if device is on (non-zero)."""
|
||||
return self._device.value > 0
|
||||
return self.device.value > 0
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn the device on."""
|
||||
|
||||
@@ -188,6 +188,11 @@ class XiaomiMiioRemote(RemoteDevice):
|
||||
"""Return the name of the remote."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
"""Return the remote object."""
|
||||
return self._device
|
||||
|
||||
@property
|
||||
def hidden(self):
|
||||
"""Return if we should hide entity."""
|
||||
@@ -208,7 +213,7 @@ class XiaomiMiioRemote(RemoteDevice):
|
||||
"""Return False if device is unreachable, else True."""
|
||||
from miio import DeviceException
|
||||
try:
|
||||
self._device.info()
|
||||
self.device.info()
|
||||
return True
|
||||
except DeviceException:
|
||||
return False
|
||||
@@ -243,7 +248,7 @@ class XiaomiMiioRemote(RemoteDevice):
|
||||
|
||||
_LOGGER.debug("Sending payload: '%s'", payload)
|
||||
try:
|
||||
self._device.play(payload)
|
||||
self.device.play(payload)
|
||||
except DeviceException as ex:
|
||||
_LOGGER.error(
|
||||
"Transmit of IR command failed, %s, exception: %s",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"state": {
|
||||
"first_quarter": "Primo quarto",
|
||||
"full_moon": "Luna piena",
|
||||
"last_quarter": "Ultimo quarto",
|
||||
"new_moon": "Nuova luna"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"state": {
|
||||
"first_quarter": "Quarto crescente",
|
||||
"full_moon": "Cheia",
|
||||
"last_quarter": "Quarto minguante",
|
||||
"new_moon": "Nova",
|
||||
"waning_crescent": "Minguante",
|
||||
"waning_gibbous": "Minguante gibosa",
|
||||
"waxing_crescent": "Crescente",
|
||||
"waxing_gibbous": "Crescente gibosa"
|
||||
}
|
||||
}
|
||||
@@ -49,14 +49,14 @@ class JuicenetSensorDevice(JuicenetDevice, Entity):
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return '{} {}'.format(self._device.name(), self._name)
|
||||
return '{} {}'.format(self.device.name(), self._name)
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon of the sensor."""
|
||||
icon = None
|
||||
if self.type == 'status':
|
||||
status = self._device.getStatus()
|
||||
status = self.device.getStatus()
|
||||
if status == 'standby':
|
||||
icon = 'mdi:power-plug-off'
|
||||
elif status == 'plugged':
|
||||
@@ -87,19 +87,19 @@ class JuicenetSensorDevice(JuicenetDevice, Entity):
|
||||
"""Return the state."""
|
||||
state = None
|
||||
if self.type == 'status':
|
||||
state = self._device.getStatus()
|
||||
state = self.device.getStatus()
|
||||
elif self.type == 'temperature':
|
||||
state = self._device.getTemperature()
|
||||
state = self.device.getTemperature()
|
||||
elif self.type == 'voltage':
|
||||
state = self._device.getVoltage()
|
||||
state = self.device.getVoltage()
|
||||
elif self.type == 'amps':
|
||||
state = self._device.getAmps()
|
||||
state = self.device.getAmps()
|
||||
elif self.type == 'watts':
|
||||
state = self._device.getWatts()
|
||||
state = self.device.getWatts()
|
||||
elif self.type == 'charge_time':
|
||||
state = self._device.getChargeTime()
|
||||
state = self.device.getChargeTime()
|
||||
elif self.type == 'energy_added':
|
||||
state = self._device.getEnergyAdded()
|
||||
state = self.device.getEnergyAdded()
|
||||
else:
|
||||
state = 'Unknown'
|
||||
return state
|
||||
@@ -109,7 +109,7 @@ class JuicenetSensorDevice(JuicenetDevice, Entity):
|
||||
"""Return the state attributes."""
|
||||
attributes = {}
|
||||
if self.type == 'status':
|
||||
man_dev_id = self._device.id()
|
||||
man_dev_id = self.device.id()
|
||||
if man_dev_id:
|
||||
attributes["manufacturer_device_id"] = man_dev_id
|
||||
return attributes
|
||||
|
||||
@@ -64,7 +64,7 @@ class KNXSensor(Entity):
|
||||
|
||||
def __init__(self, hass, device):
|
||||
"""Initialize of a KNX sensor."""
|
||||
self._device = device
|
||||
self.device = device
|
||||
self.hass = hass
|
||||
self.async_register_callbacks()
|
||||
|
||||
@@ -74,12 +74,12 @@ class KNXSensor(Entity):
|
||||
async def after_update_callback(device):
|
||||
"""Call after device was updated."""
|
||||
await self.async_update_ha_state()
|
||||
self._device.register_device_updated_cb(after_update_callback)
|
||||
self.device.register_device_updated_cb(after_update_callback)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the KNX device."""
|
||||
return self._device.name
|
||||
return self.device.name
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
@@ -94,12 +94,12 @@ class KNXSensor(Entity):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._device.resolve_state()
|
||||
return self.device.resolve_state()
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit this state is expressed in."""
|
||||
return self._device.unit_of_measurement()
|
||||
return self.device.unit_of_measurement()
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
|
||||
@@ -140,15 +140,15 @@ class NestBasicSensor(NestSensorDevice):
|
||||
self._unit = SENSOR_UNITS.get(self.variable)
|
||||
|
||||
if self.variable in VARIABLE_NAME_MAPPING:
|
||||
self._state = getattr(self._device,
|
||||
self._state = getattr(self.device,
|
||||
VARIABLE_NAME_MAPPING[self.variable])
|
||||
elif self.variable in PROTECT_SENSOR_TYPES \
|
||||
and self.variable != 'color_status':
|
||||
# keep backward compatibility
|
||||
state = getattr(self._device, self.variable)
|
||||
state = getattr(self.device, self.variable)
|
||||
self._state = state.capitalize() if state is not None else None
|
||||
else:
|
||||
self._state = getattr(self._device, self.variable)
|
||||
self._state = getattr(self.device, self.variable)
|
||||
|
||||
|
||||
class NestTempSensor(NestSensorDevice):
|
||||
@@ -166,12 +166,12 @@ class NestTempSensor(NestSensorDevice):
|
||||
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
if self._device.temperature_scale == 'C':
|
||||
if self.device.temperature_scale == 'C':
|
||||
self._unit = TEMP_CELSIUS
|
||||
else:
|
||||
self._unit = TEMP_FAHRENHEIT
|
||||
|
||||
temp = getattr(self._device, self.variable)
|
||||
temp = getattr(self.device, self.variable)
|
||||
if temp is None:
|
||||
self._state = None
|
||||
|
||||
|
||||
@@ -79,10 +79,15 @@ class TankUtilitySensor(Entity):
|
||||
self._token = token
|
||||
self._device = device
|
||||
self._state = STATE_UNKNOWN
|
||||
self._name = "Tank Utility " + self._device
|
||||
self._name = "Tank Utility " + self.device
|
||||
self._unit_of_measurement = SENSOR_UNIT_OF_MEASUREMENT
|
||||
self._attributes = {}
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
"""Return the device identifier."""
|
||||
return self._device
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
@@ -112,14 +117,14 @@ class TankUtilitySensor(Entity):
|
||||
from tank_utility import auth, device
|
||||
data = {}
|
||||
try:
|
||||
data = device.get_device_data(self._token, self._device)
|
||||
data = device.get_device_data(self._token, self.device)
|
||||
except requests.exceptions.HTTPError as http_error:
|
||||
if (http_error.response.status_code ==
|
||||
requests.codes.unauthorized): # pylint: disable=no-member
|
||||
_LOGGER.info("Getting new token")
|
||||
self._token = auth.get_token(self._email, self._password,
|
||||
force=True)
|
||||
data = device.get_device_data(self._token, self._device)
|
||||
data = device.get_device_data(self._token, self.device)
|
||||
else:
|
||||
raise http_error
|
||||
data.update(data.pop("device", {}))
|
||||
|
||||
@@ -67,7 +67,7 @@ class TelldusLiveSensor(TelldusLiveEntity):
|
||||
@property
|
||||
def _value(self):
|
||||
"""Return value of the sensor."""
|
||||
return self._device.value(*self._id[1:])
|
||||
return self.device.value(*self._id[1:])
|
||||
|
||||
@property
|
||||
def _value_as_temperature(self):
|
||||
|
||||
@@ -63,7 +63,7 @@ class KNXSwitch(SwitchDevice):
|
||||
|
||||
def __init__(self, hass, device):
|
||||
"""Initialize of KNX switch."""
|
||||
self._device = device
|
||||
self.device = device
|
||||
self.hass = hass
|
||||
self.async_register_callbacks()
|
||||
|
||||
@@ -73,12 +73,12 @@ class KNXSwitch(SwitchDevice):
|
||||
async def after_update_callback(device):
|
||||
"""Call after device was updated."""
|
||||
await self.async_update_ha_state()
|
||||
self._device.register_device_updated_cb(after_update_callback)
|
||||
self.device.register_device_updated_cb(after_update_callback)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the KNX device."""
|
||||
return self._device.name
|
||||
return self.device.name
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
@@ -93,12 +93,12 @@ class KNXSwitch(SwitchDevice):
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if device is on."""
|
||||
return self._device.state
|
||||
return self.device.state
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn the device on."""
|
||||
await self._device.set_on()
|
||||
await self.device.set_on()
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Turn the device off."""
|
||||
await self._device.set_off()
|
||||
await self.device.set_off()
|
||||
|
||||
@@ -28,14 +28,14 @@ class TelldusLiveSwitch(TelldusLiveEntity, ToggleEntity):
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if switch is on."""
|
||||
return self._device.is_on
|
||||
return self.device.is_on
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the switch on."""
|
||||
self._device.turn_on()
|
||||
self.device.turn_on()
|
||||
self.changed()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the switch off."""
|
||||
self._device.turn_off()
|
||||
self.device.turn_off()
|
||||
self.changed()
|
||||
|
||||
@@ -287,14 +287,14 @@ 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
|
||||
self.device = self._client.device(device_id)
|
||||
self._name = self.device.name
|
||||
_LOGGER.debug('Created device %s', self)
|
||||
|
||||
def changed(self):
|
||||
"""Return the property of the device might have changed."""
|
||||
if self._device.name:
|
||||
self._name = self._device.name
|
||||
if self.device.name:
|
||||
self._name = self.device.name
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
@@ -302,10 +302,15 @@ class TelldusLiveEntity(Entity):
|
||||
"""Return the id of the device."""
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
"""Return the representation of the device."""
|
||||
return self._client.device(self.device_id)
|
||||
|
||||
@property
|
||||
def _state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._device.state
|
||||
return self.device.state
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@@ -343,16 +348,16 @@ class TelldusLiveEntity(Entity):
|
||||
from tellduslive import (BATTERY_LOW,
|
||||
BATTERY_UNKNOWN,
|
||||
BATTERY_OK)
|
||||
if self._device.battery == BATTERY_LOW:
|
||||
if self.device.battery == BATTERY_LOW:
|
||||
return 1
|
||||
if self._device.battery == BATTERY_UNKNOWN:
|
||||
if self.device.battery == BATTERY_UNKNOWN:
|
||||
return None
|
||||
if self._device.battery == BATTERY_OK:
|
||||
if self.device.battery == BATTERY_OK:
|
||||
return 100
|
||||
return self._device.battery # Percentage
|
||||
return self.device.battery # Percentage
|
||||
|
||||
@property
|
||||
def _last_updated(self):
|
||||
"""Return the last update of a device."""
|
||||
return str(datetime.fromtimestamp(self._device.lastUpdated)) \
|
||||
if self._device.lastUpdated else None
|
||||
return str(datetime.fromtimestamp(self.device.lastUpdated)) \
|
||||
if self.device.lastUpdated else None
|
||||
|
||||
@@ -427,10 +427,14 @@ async def async_process_ha_core_config(
|
||||
if has_trusted_networks:
|
||||
auth_conf.append({'type': 'trusted_networks'})
|
||||
|
||||
mfa_conf = config.get(CONF_AUTH_MFA_MODULES, [
|
||||
{'type': 'totp', 'id': 'totp', 'name': 'Authenticator app'}
|
||||
])
|
||||
|
||||
setattr(hass, 'auth', await auth.auth_manager_from_config(
|
||||
hass,
|
||||
auth_conf,
|
||||
config.get(CONF_AUTH_MFA_MODULES, [])))
|
||||
mfa_conf))
|
||||
|
||||
hac = hass.config
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"""Constants used by Home Assistant components."""
|
||||
MAJOR_VERSION = 0
|
||||
MINOR_VERSION = 77
|
||||
PATCH_VERSION = '0b1'
|
||||
PATCH_VERSION = '0b2'
|
||||
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
|
||||
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
|
||||
REQUIRED_PYTHON_VER = (3, 5, 3)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
aiohttp==3.3.2
|
||||
aiohttp==3.4.0
|
||||
astral==1.6.1
|
||||
async_timeout==3.0.0
|
||||
attrs==18.1.0
|
||||
bcrypt==3.1.4
|
||||
certifi>=2018.04.16
|
||||
jinja2>=2.10
|
||||
PyJWT==1.6.4
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# Home Assistant core
|
||||
aiohttp==3.3.2
|
||||
aiohttp==3.4.0
|
||||
astral==1.6.1
|
||||
async_timeout==3.0.0
|
||||
attrs==18.1.0
|
||||
bcrypt==3.1.4
|
||||
certifi>=2018.04.16
|
||||
jinja2>=2.10
|
||||
PyJWT==1.6.4
|
||||
@@ -46,6 +47,9 @@ PyMVGLive==1.1.4
|
||||
# homeassistant.components.arduino
|
||||
PyMata==2.14
|
||||
|
||||
# homeassistant.auth.mfa_modules.totp
|
||||
PyQRCode==1.2.1
|
||||
|
||||
# homeassistant.components.sensor.rmvtransport
|
||||
PyRMVtransport==0.0.7
|
||||
|
||||
@@ -438,7 +442,7 @@ hole==0.3.0
|
||||
holidays==0.9.6
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20180825.0
|
||||
home-assistant-frontend==20180826.0
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
# homekit==0.10
|
||||
@@ -985,6 +989,7 @@ pyopenuv==1.0.1
|
||||
# homeassistant.components.iota
|
||||
pyota==2.0.5
|
||||
|
||||
# homeassistant.auth.mfa_modules.totp
|
||||
# homeassistant.components.sensor.otp
|
||||
pyotp==2.2.6
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ hbmqtt==0.9.2
|
||||
holidays==0.9.6
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20180825.0
|
||||
home-assistant-frontend==20180826.0
|
||||
|
||||
# homeassistant.components.homematicip_cloud
|
||||
homematicip==0.9.8
|
||||
@@ -154,6 +154,10 @@ pymonoprice==0.3
|
||||
# homeassistant.components.binary_sensor.nx584
|
||||
pynx584==0.4
|
||||
|
||||
# homeassistant.auth.mfa_modules.totp
|
||||
# homeassistant.components.sensor.otp
|
||||
pyotp==2.2.6
|
||||
|
||||
# homeassistant.components.qwikswitch
|
||||
pyqwikswitch==0.8
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@ TEST_REQUIREMENTS = (
|
||||
'pylitejet',
|
||||
'pymonoprice',
|
||||
'pynx584',
|
||||
'pyotp',
|
||||
'pyqwikswitch',
|
||||
'PyRMVtransport',
|
||||
'python-forecastio',
|
||||
|
||||
3
setup.py
3
setup.py
@@ -32,10 +32,11 @@ PROJECT_URLS = {
|
||||
PACKAGES = find_packages(exclude=['tests', 'tests.*'])
|
||||
|
||||
REQUIRES = [
|
||||
'aiohttp==3.3.2',
|
||||
'aiohttp==3.4.0',
|
||||
'astral==1.6.1',
|
||||
'async_timeout==3.0.0',
|
||||
'attrs==18.1.0',
|
||||
'bcrypt==3.1.4',
|
||||
'certifi>=2018.04.16',
|
||||
'jinja2>=2.10',
|
||||
'PyJWT==1.6.4',
|
||||
|
||||
130
tests/auth/mfa_modules/test_totp.py
Normal file
130
tests/auth/mfa_modules/test_totp.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""Test the Time-based One Time Password (MFA) auth module."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.auth import models as auth_models, auth_manager_from_config
|
||||
from homeassistant.auth.mfa_modules import auth_mfa_module_from_config
|
||||
from tests.common import MockUser
|
||||
|
||||
MOCK_CODE = '123456'
|
||||
|
||||
|
||||
async def test_validating_mfa(hass):
|
||||
"""Test validating mfa code."""
|
||||
totp_auth_module = await auth_mfa_module_from_config(hass, {
|
||||
'type': 'totp'
|
||||
})
|
||||
await totp_auth_module.async_setup_user('test-user', {})
|
||||
|
||||
with patch('pyotp.TOTP.verify', return_value=True):
|
||||
assert await totp_auth_module.async_validation(
|
||||
'test-user', {'code': MOCK_CODE})
|
||||
|
||||
|
||||
async def test_validating_mfa_invalid_code(hass):
|
||||
"""Test validating an invalid mfa code."""
|
||||
totp_auth_module = await auth_mfa_module_from_config(hass, {
|
||||
'type': 'totp'
|
||||
})
|
||||
await totp_auth_module.async_setup_user('test-user', {})
|
||||
|
||||
with patch('pyotp.TOTP.verify', return_value=False):
|
||||
assert await totp_auth_module.async_validation(
|
||||
'test-user', {'code': MOCK_CODE}) is False
|
||||
|
||||
|
||||
async def test_validating_mfa_invalid_user(hass):
|
||||
"""Test validating an mfa code with invalid user."""
|
||||
totp_auth_module = await auth_mfa_module_from_config(hass, {
|
||||
'type': 'totp'
|
||||
})
|
||||
await totp_auth_module.async_setup_user('test-user', {})
|
||||
|
||||
assert await totp_auth_module.async_validation(
|
||||
'invalid-user', {'code': MOCK_CODE}) is False
|
||||
|
||||
|
||||
async def test_setup_depose_user(hass):
|
||||
"""Test despose user."""
|
||||
totp_auth_module = await auth_mfa_module_from_config(hass, {
|
||||
'type': 'totp'
|
||||
})
|
||||
result = await totp_auth_module.async_setup_user('test-user', {})
|
||||
assert len(totp_auth_module._users) == 1
|
||||
result2 = await totp_auth_module.async_setup_user('test-user', {})
|
||||
assert len(totp_auth_module._users) == 1
|
||||
assert result != result2
|
||||
|
||||
await totp_auth_module.async_depose_user('test-user')
|
||||
assert len(totp_auth_module._users) == 0
|
||||
|
||||
result = await totp_auth_module.async_setup_user(
|
||||
'test-user2', {'secret': 'secret-code'})
|
||||
assert result == 'secret-code'
|
||||
assert len(totp_auth_module._users) == 1
|
||||
|
||||
|
||||
async def test_login_flow_validates_mfa(hass):
|
||||
"""Test login flow with mfa enabled."""
|
||||
hass.auth = await auth_manager_from_config(hass, [{
|
||||
'type': 'insecure_example',
|
||||
'users': [{'username': 'test-user', 'password': 'test-pass'}],
|
||||
}], [{
|
||||
'type': 'totp',
|
||||
}])
|
||||
user = MockUser(
|
||||
id='mock-user',
|
||||
is_owner=False,
|
||||
is_active=False,
|
||||
name='Paulus',
|
||||
).add_to_auth_manager(hass.auth)
|
||||
await hass.auth.async_link_user(user, auth_models.Credentials(
|
||||
id='mock-id',
|
||||
auth_provider_type='insecure_example',
|
||||
auth_provider_id=None,
|
||||
data={'username': 'test-user'},
|
||||
is_new=False,
|
||||
))
|
||||
|
||||
await hass.auth.async_enable_user_mfa(user, 'totp', {})
|
||||
|
||||
provider = hass.auth.auth_providers[0]
|
||||
|
||||
result = await hass.auth.login_flow.async_init(
|
||||
(provider.type, provider.id))
|
||||
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
result = await hass.auth.login_flow.async_configure(result['flow_id'], {
|
||||
'username': 'incorrect-user',
|
||||
'password': 'test-pass',
|
||||
})
|
||||
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result['errors']['base'] == 'invalid_auth'
|
||||
|
||||
result = await hass.auth.login_flow.async_configure(result['flow_id'], {
|
||||
'username': 'test-user',
|
||||
'password': 'incorrect-pass',
|
||||
})
|
||||
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result['errors']['base'] == 'invalid_auth'
|
||||
|
||||
result = await hass.auth.login_flow.async_configure(result['flow_id'], {
|
||||
'username': 'test-user',
|
||||
'password': 'test-pass',
|
||||
})
|
||||
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result['step_id'] == 'mfa'
|
||||
assert result['data_schema'].schema.get('code') == str
|
||||
|
||||
with patch('pyotp.TOTP.verify', return_value=False):
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
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'
|
||||
|
||||
with patch('pyotp.TOTP.verify', return_value=True):
|
||||
result = await hass.auth.login_flow.async_configure(
|
||||
result['flow_id'], {'code': MOCK_CODE})
|
||||
assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result['data'].id == 'mock-user'
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Test the Home Assistant local auth provider."""
|
||||
from unittest.mock import Mock
|
||||
|
||||
import base64
|
||||
import pytest
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
@@ -132,3 +133,91 @@ async def test_new_users_populate_values(hass, data):
|
||||
user = await manager.async_get_or_create_user(credentials)
|
||||
assert user.name == 'hello'
|
||||
assert user.is_active
|
||||
|
||||
|
||||
async def test_new_hashes_are_bcrypt(data, hass):
|
||||
"""Test that newly created hashes are using bcrypt."""
|
||||
data.add_auth('newuser', 'newpass')
|
||||
found = None
|
||||
for user in data.users:
|
||||
if user['username'] == 'newuser':
|
||||
found = user
|
||||
assert found is not None
|
||||
user_hash = base64.b64decode(found['password'])
|
||||
assert (user_hash.startswith(b'$2a$')
|
||||
or user_hash.startswith(b'$2b$')
|
||||
or user_hash.startswith(b'$2x$')
|
||||
or user_hash.startswith(b'$2y$'))
|
||||
|
||||
|
||||
async def test_pbkdf2_to_bcrypt_hash_upgrade(hass_storage, hass):
|
||||
"""Test migrating user from pbkdf2 hash to bcrypt hash."""
|
||||
hass_storage[hass_auth.STORAGE_KEY] = {
|
||||
'version': hass_auth.STORAGE_VERSION,
|
||||
'key': hass_auth.STORAGE_KEY,
|
||||
'data': {
|
||||
'salt': '09c52f0b120eaa7dea5f73f9a9b985f3d493b30a08f3f2945ef613'
|
||||
'0b08e6a3ea',
|
||||
'users': [
|
||||
{
|
||||
'password': 'L5PAbehB8LAQI2Ixu+d+PDNJKmljqLnBcYWYw35onC/8D'
|
||||
'BM1SpvT6A8ZFael5+deCt+s+43J08IcztnguouHSw==',
|
||||
'username': 'legacyuser'
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
data = hass_auth.Data(hass)
|
||||
await data.async_load()
|
||||
|
||||
# verify the correct (pbkdf2) password successfuly authenticates the user
|
||||
await hass.async_add_executor_job(
|
||||
data.validate_login, 'legacyuser', 'beer')
|
||||
|
||||
# ...and that the hashes are now bcrypt hashes
|
||||
user_hash = base64.b64decode(
|
||||
hass_storage[hass_auth.STORAGE_KEY]['data']['users'][0]['password'])
|
||||
assert (user_hash.startswith(b'$2a$')
|
||||
or user_hash.startswith(b'$2b$')
|
||||
or user_hash.startswith(b'$2x$')
|
||||
or user_hash.startswith(b'$2y$'))
|
||||
|
||||
|
||||
async def test_pbkdf2_to_bcrypt_hash_upgrade_with_incorrect_pass(hass_storage,
|
||||
hass):
|
||||
"""Test migrating user from pbkdf2 hash to bcrypt hash."""
|
||||
hass_storage[hass_auth.STORAGE_KEY] = {
|
||||
'version': hass_auth.STORAGE_VERSION,
|
||||
'key': hass_auth.STORAGE_KEY,
|
||||
'data': {
|
||||
'salt': '09c52f0b120eaa7dea5f73f9a9b985f3d493b30a08f3f2945ef613'
|
||||
'0b08e6a3ea',
|
||||
'users': [
|
||||
{
|
||||
'password': 'L5PAbehB8LAQI2Ixu+d+PDNJKmljqLnBcYWYw35onC/8D'
|
||||
'BM1SpvT6A8ZFael5+deCt+s+43J08IcztnguouHSw==',
|
||||
'username': 'legacyuser'
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
data = hass_auth.Data(hass)
|
||||
await data.async_load()
|
||||
|
||||
orig_user_hash = base64.b64decode(
|
||||
hass_storage[hass_auth.STORAGE_KEY]['data']['users'][0]['password'])
|
||||
|
||||
# Make sure invalid legacy passwords fail
|
||||
with pytest.raises(hass_auth.InvalidAuth):
|
||||
await hass.async_add_executor_job(
|
||||
data.validate_login, 'legacyuser', 'wine')
|
||||
|
||||
# Make sure we don't change the password/hash when password is incorrect
|
||||
with pytest.raises(hass_auth.InvalidAuth):
|
||||
await hass.async_add_executor_job(
|
||||
data.validate_login, 'legacyuser', 'wine')
|
||||
|
||||
same_user_hash = base64.b64decode(
|
||||
hass_storage[hass_auth.STORAGE_KEY]['data']['users'][0]['password'])
|
||||
|
||||
assert orig_user_hash == same_user_hash
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.const import (
|
||||
CONF_LATITUDE, CONF_LONGITUDE, CONF_UNIT_SYSTEM, CONF_NAME,
|
||||
CONF_TIME_ZONE, CONF_ELEVATION, CONF_CUSTOMIZE, __version__,
|
||||
CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT,
|
||||
CONF_AUTH_PROVIDERS)
|
||||
CONF_AUTH_PROVIDERS, CONF_AUTH_MFA_MODULES)
|
||||
from homeassistant.util import location as location_util, dt as dt_util
|
||||
from homeassistant.util.yaml import SECRET_YAML
|
||||
from homeassistant.util.async_ import run_coroutine_threadsafe
|
||||
@@ -805,6 +805,10 @@ async def test_auth_provider_config(hass):
|
||||
CONF_AUTH_PROVIDERS: [
|
||||
{'type': 'homeassistant'},
|
||||
{'type': 'legacy_api_password'},
|
||||
],
|
||||
CONF_AUTH_MFA_MODULES: [
|
||||
{'type': 'totp'},
|
||||
{'type': 'totp', 'id': 'second'},
|
||||
]
|
||||
}
|
||||
if hasattr(hass, 'auth'):
|
||||
@@ -815,6 +819,9 @@ async def test_auth_provider_config(hass):
|
||||
assert hass.auth.auth_providers[0].type == 'homeassistant'
|
||||
assert hass.auth.auth_providers[1].type == 'legacy_api_password'
|
||||
assert hass.auth.active is True
|
||||
assert len(hass.auth.auth_mfa_modules) == 2
|
||||
assert hass.auth.auth_mfa_modules[0].id == 'totp'
|
||||
assert hass.auth.auth_mfa_modules[1].id == 'second'
|
||||
|
||||
|
||||
async def test_auth_provider_config_default(hass):
|
||||
@@ -834,6 +841,8 @@ async def test_auth_provider_config_default(hass):
|
||||
assert len(hass.auth.auth_providers) == 1
|
||||
assert hass.auth.auth_providers[0].type == 'homeassistant'
|
||||
assert hass.auth.active is True
|
||||
assert len(hass.auth.auth_mfa_modules) == 1
|
||||
assert hass.auth.auth_mfa_modules[0].id == 'totp'
|
||||
|
||||
|
||||
async def test_auth_provider_config_default_api_password(hass):
|
||||
|
||||
Reference in New Issue
Block a user