From e8775ba2b4c27018872b977d6874bc177d9cb186 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Fri, 24 Aug 2018 10:17:43 -0700 Subject: [PATCH 001/172] Add multi-factor auth module setup flow (#16141) * Add mfa setup flow * Lint * Address code review comment * Fix unit test * Add assertion for WS response ordering * Missed a return * Remove setup_schema from MFA base class * Move auth.util.validate_current_user -> webscoket_api.ws_require_user --- homeassistant/auth/__init__.py | 9 -- homeassistant/auth/mfa_modules/__init__.py | 49 ++++++- .../auth/mfa_modules/insecure_example.py | 15 +- homeassistant/components/auth/__init__.py | 40 ++++-- .../components/auth/mfa_setup_flow.py | 134 ++++++++++++++++++ homeassistant/components/websocket_api.py | 58 +++++++- .../auth/mfa_modules/test_insecure_example.py | 18 +++ tests/components/auth/test_mfa_setup_flow.py | 99 +++++++++++++ 8 files changed, 386 insertions(+), 36 deletions(-) create mode 100644 homeassistant/components/auth/mfa_setup_flow.py create mode 100644 tests/components/auth/test_mfa_setup_flow.py diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index b5ba869cdf1..e0b7b377b1f 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -6,8 +6,6 @@ from typing import Any, Dict, List, Optional, Tuple, cast import jwt -import voluptuous as vol - from homeassistant import data_entry_flow from homeassistant.core import callback, HomeAssistant from homeassistant.util import dt as dt_util @@ -235,13 +233,6 @@ class AuthManager: raise ValueError('Unable find multi-factor auth module: {}' .format(mfa_module_id)) - if module.setup_schema is not None: - try: - # pylint: disable=not-callable - data = module.setup_schema(data) - except vol.Invalid as err: - raise ValueError('Data does not match schema: {}'.format(err)) - await module.async_setup_user(user.id, data) async def async_disable_user_mfa(self, user: models.User, diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index d0707c4a745..cb0758e3ef8 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -8,7 +8,7 @@ from typing import Any, Dict, Optional import voluptuous as vol from voluptuous.humanize import humanize_error -from homeassistant import requirements +from homeassistant import requirements, data_entry_flow from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant from homeassistant.util.decorator import Registry @@ -64,15 +64,14 @@ class MultiFactorAuthModule: """Return a voluptuous schema to define mfa auth module's input.""" raise NotImplementedError - @property - def setup_schema(self) -> Optional[vol.Schema]: - """Return a vol schema to validate mfa auth module's setup input. + async def async_setup_flow(self, user_id: str) -> 'SetupFlow': + """Return a data entry flow handler for setup module. - Optional + Mfa module should extend SetupFlow """ - return None + raise NotImplementedError - async def async_setup_user(self, user_id: str, setup_data: Any) -> None: + async def async_setup_user(self, user_id: str, setup_data: Any) -> Any: """Set up user for mfa auth module.""" raise NotImplementedError @@ -90,6 +89,42 @@ class MultiFactorAuthModule: raise NotImplementedError +class SetupFlow(data_entry_flow.FlowHandler): + """Handler for the setup flow.""" + + def __init__(self, auth_module: MultiFactorAuthModule, + setup_schema: vol.Schema, + user_id: str) -> None: + """Initialize the setup flow.""" + self._auth_module = auth_module + self._setup_schema = setup_schema + self._user_id = user_id + + 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. + """ + errors = {} # type: Dict[str, str] + + if user_input: + result = await self._auth_module.async_setup_user( + self._user_id, user_input) + return self.async_create_entry( + title=self._auth_module.name, + data={'result': result} + ) + + return self.async_show_form( + step_id='init', + data_schema=self._setup_schema, + errors=errors + ) + + async def auth_mfa_module_from_config( hass: HomeAssistant, config: Dict[str, Any]) \ -> Optional[MultiFactorAuthModule]: diff --git a/homeassistant/auth/mfa_modules/insecure_example.py b/homeassistant/auth/mfa_modules/insecure_example.py index 59b3f64d2e0..9c72111ef96 100644 --- a/homeassistant/auth/mfa_modules/insecure_example.py +++ b/homeassistant/auth/mfa_modules/insecure_example.py @@ -1,13 +1,13 @@ """Example auth module.""" import logging -from typing import Any, Dict, Optional +from typing import Any, Dict import voluptuous as vol from homeassistant.core import HomeAssistant from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \ - MULTI_FACTOR_AUTH_MODULE_SCHEMA + MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({ vol.Required('data'): [vol.Schema({ @@ -36,11 +36,18 @@ class InsecureExampleModule(MultiFactorAuthModule): return vol.Schema({'pin': str}) @property - def setup_schema(self) -> Optional[vol.Schema]: + def setup_schema(self) -> vol.Schema: """Validate async_setup_user input data.""" return vol.Schema({'pin': str}) - async def async_setup_user(self, user_id: str, setup_data: Any) -> None: + async def async_setup_flow(self, user_id: str) -> SetupFlow: + """Return a data entry flow handler for setup module. + + Mfa module should extend SetupFlow + """ + return SetupFlow(self, self.setup_schema, user_id) + + async def async_setup_user(self, user_id: str, setup_data: Any) -> Any: """Set up user to use mfa module.""" # data shall has been validate in caller pin = setup_data['pin'] diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index 4251b23e514..a87e646761c 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -68,10 +68,12 @@ from homeassistant.components import websocket_api from homeassistant.components.http.ban import log_invalid_auth from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView -from homeassistant.core import callback +from homeassistant.core import callback, HomeAssistant from homeassistant.util import dt as dt_util + from . import indieauth from . import login_flow +from . import mfa_setup_flow DOMAIN = 'auth' DEPENDENCIES = ['http'] @@ -100,6 +102,7 @@ async def async_setup(hass, config): ) await login_flow.async_setup(hass, store_result) + await mfa_setup_flow.async_setup(hass) return True @@ -315,21 +318,28 @@ def _create_auth_code_store(): return store_result, retrieve_result +@websocket_api.ws_require_user() @callback -def websocket_current_user(hass, connection, msg): +def websocket_current_user( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg): """Return the current user.""" - user = connection.request.get('hass_user') + async def async_get_current_user(user): + """Get current user.""" + enabled_modules = await hass.auth.async_get_enabled_mfa(user) - if user is None: - connection.to_write.put_nowait(websocket_api.error_message( - msg['id'], 'no_user', 'Not authenticated as a user')) - return + connection.send_message_outside( + websocket_api.result_message(msg['id'], { + 'id': user.id, + 'name': user.name, + 'is_owner': user.is_owner, + 'credentials': [{'auth_provider_type': c.auth_provider_type, + 'auth_provider_id': c.auth_provider_id} + for c in user.credentials], + 'mfa_modules': [{ + 'id': module.id, + 'name': module.name, + 'enabled': module.id in enabled_modules, + } for module in hass.auth.auth_mfa_modules], + })) - connection.to_write.put_nowait(websocket_api.result_message(msg['id'], { - 'id': user.id, - 'name': user.name, - 'is_owner': user.is_owner, - 'credentials': [{'auth_provider_type': c.auth_provider_type, - 'auth_provider_id': c.auth_provider_id} - for c in user.credentials] - })) + hass.async_create_task(async_get_current_user(connection.user)) diff --git a/homeassistant/components/auth/mfa_setup_flow.py b/homeassistant/components/auth/mfa_setup_flow.py new file mode 100644 index 00000000000..82eb913d890 --- /dev/null +++ b/homeassistant/components/auth/mfa_setup_flow.py @@ -0,0 +1,134 @@ +"""Helpers to setup multi-factor auth module.""" +import logging + +import voluptuous as vol + +from homeassistant import data_entry_flow +from homeassistant.components import websocket_api +from homeassistant.core import callback, HomeAssistant + +WS_TYPE_SETUP_MFA = 'auth/setup_mfa' +SCHEMA_WS_SETUP_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_SETUP_MFA, + vol.Exclusive('mfa_module_id', 'module_or_flow_id'): str, + vol.Exclusive('flow_id', 'module_or_flow_id'): str, + vol.Optional('user_input'): object, +}) + +WS_TYPE_DEPOSE_MFA = 'auth/depose_mfa' +SCHEMA_WS_DEPOSE_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_DEPOSE_MFA, + vol.Required('mfa_module_id'): str, +}) + +DATA_SETUP_FLOW_MGR = 'auth_mfa_setup_flow_manager' + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass): + """Init mfa setup flow manager.""" + async def _async_create_setup_flow(handler, context, data): + """Create a setup flow. hanlder is a mfa module.""" + mfa_module = hass.auth.get_auth_mfa_module(handler) + if mfa_module is None: + raise ValueError('Mfa module {} is not found'.format(handler)) + + user_id = data.pop('user_id') + return await mfa_module.async_setup_flow(user_id) + + async def _async_finish_setup_flow(flow, flow_result): + _LOGGER.debug('flow_result: %s', flow_result) + return flow_result + + hass.data[DATA_SETUP_FLOW_MGR] = data_entry_flow.FlowManager( + hass, _async_create_setup_flow, _async_finish_setup_flow) + + hass.components.websocket_api.async_register_command( + WS_TYPE_SETUP_MFA, websocket_setup_mfa, SCHEMA_WS_SETUP_MFA) + + hass.components.websocket_api.async_register_command( + WS_TYPE_DEPOSE_MFA, websocket_depose_mfa, SCHEMA_WS_DEPOSE_MFA) + + +@callback +@websocket_api.ws_require_user(allow_system_user=False) +def websocket_setup_mfa( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg): + """Return a setup flow for mfa auth module.""" + async def async_setup_flow(msg): + """Return a setup flow for mfa auth module.""" + flow_manager = hass.data[DATA_SETUP_FLOW_MGR] + + flow_id = msg.get('flow_id') + if flow_id is not None: + result = await flow_manager.async_configure( + flow_id, msg.get('user_input')) + connection.send_message_outside( + websocket_api.result_message( + msg['id'], _prepare_result_json(result))) + return + + mfa_module_id = msg.get('mfa_module_id') + mfa_module = hass.auth.get_auth_mfa_module(mfa_module_id) + if mfa_module is None: + connection.send_message_outside(websocket_api.error_message( + msg['id'], 'no_module', + 'MFA module {} is not found'.format(mfa_module_id))) + return + + result = await flow_manager.async_init( + mfa_module_id, data={'user_id': connection.user.id}) + + connection.send_message_outside( + websocket_api.result_message( + msg['id'], _prepare_result_json(result))) + + hass.async_create_task(async_setup_flow(msg)) + + +@callback +@websocket_api.ws_require_user(allow_system_user=False) +def websocket_depose_mfa( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg): + """Remove user from mfa module.""" + async def async_depose(msg): + """Remove user from mfa auth module.""" + mfa_module_id = msg['mfa_module_id'] + try: + await hass.auth.async_disable_user_mfa( + connection.user, msg['mfa_module_id']) + except ValueError as err: + connection.send_message_outside(websocket_api.error_message( + msg['id'], 'disable_failed', + 'Cannot disable MFA Module {}: {}'.format( + mfa_module_id, err))) + return + + connection.send_message_outside( + websocket_api.result_message( + msg['id'], 'done')) + + hass.async_create_task(async_depose(msg)) + + +def _prepare_result_json(result): + """Convert result to JSON.""" + if result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + data = result.copy() + return data + + if result['type'] != data_entry_flow.RESULT_TYPE_FORM: + return result + + import voluptuous_serialize + + data = result.copy() + + schema = data['data_schema'] + if schema is None: + data['data_schema'] = [] + else: + data['data_schema'] = voluptuous_serialize.convert(schema) + + return data diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index 1ba0e20d553..0c9ab366534 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -18,7 +18,7 @@ from voluptuous.humanize import humanize_error from homeassistant.const import ( MATCH_ALL, EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, __version__) -from homeassistant.core import Context, callback +from homeassistant.core import Context, callback, HomeAssistant from homeassistant.loader import bind_hass from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers import config_validation as cv @@ -576,3 +576,59 @@ def handle_ping(hass, connection, msg): Async friendly. """ connection.to_write.put_nowait(pong_message(msg['id'])) + + +def ws_require_user( + only_owner=False, only_system_user=False, allow_system_user=True, + only_active_user=True, only_inactive_user=False): + """Decorate function validating login user exist in current WS connection. + + Will write out error message if not authenticated. + """ + def validator(func): + """Decorate func.""" + @wraps(func) + def check_current_user(hass: HomeAssistant, + connection: ActiveConnection, + msg): + """Check current user.""" + def output_error(message_id, message): + """Output error message.""" + connection.send_message_outside(error_message( + msg['id'], message_id, message)) + + if connection.user is None: + output_error('no_user', 'Not authenticated as a user') + return + + if only_owner and not connection.user.is_owner: + output_error('only_owner', 'Only allowed as owner') + return + + if (only_system_user and + not connection.user.system_generated): + output_error('only_system_user', + 'Only allowed as system user') + return + + if (not allow_system_user + and connection.user.system_generated): + output_error('not_system_user', 'Not allowed as system user') + return + + if (only_active_user and + not connection.user.is_active): + output_error('only_active_user', + 'Only allowed as active user') + return + + if only_inactive_user and connection.user.is_active: + output_error('only_inactive_user', + 'Not allowed as active user') + return + + return func(hass, connection, msg) + + return check_current_user + + return validator diff --git a/tests/auth/mfa_modules/test_insecure_example.py b/tests/auth/mfa_modules/test_insecure_example.py index 9d90532728a..e6f83762cd7 100644 --- a/tests/auth/mfa_modules/test_insecure_example.py +++ b/tests/auth/mfa_modules/test_insecure_example.py @@ -125,3 +125,21 @@ async def test_login(hass): result['flow_id'], {'pin': '123456'}) assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result['data'].id == 'mock-user' + + +async def test_setup_flow(hass): + """Test validating pin.""" + auth_module = await auth_mfa_module_from_config(hass, { + 'type': 'insecure_example', + 'data': [{'user_id': 'test-user', 'pin': '123456'}] + }) + + flow = await auth_module.async_setup_flow('new-user') + + result = await flow.async_step_init() + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + + result = await flow.async_step_init({'pin': 'abcdefg'}) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert auth_module._data[1]['user_id'] == 'new-user' + assert auth_module._data[1]['pin'] == 'abcdefg' diff --git a/tests/components/auth/test_mfa_setup_flow.py b/tests/components/auth/test_mfa_setup_flow.py new file mode 100644 index 00000000000..93b5cdf7bb9 --- /dev/null +++ b/tests/components/auth/test_mfa_setup_flow.py @@ -0,0 +1,99 @@ +"""Tests for the mfa setup flow.""" +from homeassistant import data_entry_flow +from homeassistant.auth import auth_manager_from_config +from homeassistant.components.auth import mfa_setup_flow +from homeassistant.setup import async_setup_component + +from tests.common import MockUser, CLIENT_ID, ensure_auth_manager_loaded + + +async def test_ws_setup_depose_mfa(hass, hass_ws_client): + """Test set up mfa module for current user.""" + hass.auth = await auth_manager_from_config( + hass, provider_configs=[{ + 'type': 'insecure_example', + 'users': [{ + 'username': 'test-user', + 'password': 'test-pass', + 'name': 'Test Name', + }] + }], module_configs=[{ + 'type': 'insecure_example', + 'id': 'example_module', + 'data': [{'user_id': 'mock-user', 'pin': '123456'}] + }]) + ensure_auth_manager_loaded(hass.auth) + await async_setup_component(hass, 'auth', {'http': {}}) + + user = MockUser(id='mock-user').add_to_hass(hass) + cred = await hass.auth.auth_providers[0].async_get_or_create_credentials( + {'username': 'test-user'}) + await hass.auth.async_link_user(user, cred) + refresh_token = await hass.auth.async_create_refresh_token(user, CLIENT_ID) + access_token = hass.auth.async_create_access_token(refresh_token) + + client = await hass_ws_client(hass, access_token) + + await client.send_json({ + 'id': 10, + 'type': mfa_setup_flow.WS_TYPE_SETUP_MFA, + }) + + result = await client.receive_json() + assert result['id'] == 10 + assert result['success'] is False + assert result['error']['code'] == 'no_module' + + await client.send_json({ + 'id': 11, + 'type': mfa_setup_flow.WS_TYPE_SETUP_MFA, + 'mfa_module_id': 'example_module', + }) + + result = await client.receive_json() + assert result['id'] == 11 + assert result['success'] + + flow = result['result'] + assert flow['type'] == data_entry_flow.RESULT_TYPE_FORM + assert flow['handler'] == 'example_module' + assert flow['step_id'] == 'init' + assert flow['data_schema'][0] == {'type': 'string', 'name': 'pin'} + + await client.send_json({ + 'id': 12, + 'type': mfa_setup_flow.WS_TYPE_SETUP_MFA, + 'flow_id': flow['flow_id'], + 'user_input': {'pin': '654321'}, + }) + + result = await client.receive_json() + assert result['id'] == 12 + assert result['success'] + + flow = result['result'] + assert flow['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert flow['handler'] == 'example_module' + assert flow['data']['result'] is None + + await client.send_json({ + 'id': 13, + 'type': mfa_setup_flow.WS_TYPE_DEPOSE_MFA, + 'mfa_module_id': 'invalid_id', + }) + + result = await client.receive_json() + assert result['id'] == 13 + assert result['success'] is False + assert result['error']['code'] == 'disable_failed' + + await client.send_json({ + 'id': 14, + 'type': mfa_setup_flow.WS_TYPE_DEPOSE_MFA, + 'mfa_module_id': 'example_module', + }) + + result = await client.receive_json() + assert result['id'] == 14 + assert result['success'] + assert result['result'] == 'done' From e91a1529e43a9ce08a9858d4fce59f4bcc7607e0 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 24 Aug 2018 19:37:22 +0200 Subject: [PATCH 002/172] deCONZ - Support device registry (#16115) Add support for device registry in deCONZ component --- .../components/binary_sensor/deconz.py | 19 ++++++- homeassistant/components/deconz/__init__.py | 11 +++- homeassistant/components/deconz/const.py | 1 + homeassistant/components/light/deconz.py | 19 ++++++- homeassistant/components/sensor/deconz.py | 35 ++++++++++++- homeassistant/components/switch/deconz.py | 19 ++++++- homeassistant/helpers/device_registry.py | 13 +++-- homeassistant/helpers/entity_platform.py | 7 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/deconz/test_init.py | 50 +++++++++++++------ tests/helpers/test_device_registry.py | 20 +++++--- 12 files changed, 162 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py index d3d27c05333..9aa0c446f2b 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -7,9 +7,10 @@ https://home-assistant.io/components/binary_sensor.deconz/ from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.deconz.const import ( ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ, - DATA_DECONZ_ID, DATA_DECONZ_UNSUB) + DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DECONZ_DOMAIN) from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.core import callback +from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.dispatcher import async_dispatcher_connect DEPENDENCIES = ['deconz'] @@ -113,3 +114,19 @@ class DeconzBinarySensor(BinarySensorDevice): if self._sensor.type in PRESENCE and self._sensor.dark is not None: attr[ATTR_DARK] = self._sensor.dark return attr + + @property + def device(self): + """Return a device description for device registry.""" + if (self._sensor.uniqueid is None or + self._sensor.uniqueid.count(':') != 7): + return None + serial = self._sensor.uniqueid.split('-', 1)[0] + return { + 'connection': [[CONNECTION_ZIGBEE, serial]], + 'identifiers': [[DECONZ_DOMAIN, serial]], + 'manufacturer': self._sensor.manufacturer, + 'model': self._sensor.modelid, + 'name': self._sensor.name, + 'sw_version': self._sensor.swversion, + } diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index cf8d891661e..d435e9e3c04 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_ID, CONF_PORT, EVENT_HOMEASSISTANT_STOP) from homeassistant.core import EventOrigin, callback from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send) from homeassistant.util import slugify @@ -23,7 +24,7 @@ from .const import ( CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER) -REQUIREMENTS = ['pydeconz==43'] +REQUIREMENTS = ['pydeconz==44'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -119,6 +120,14 @@ async def async_setup_entry(hass, config_entry): deconz.start() + device_registry = await \ + hass.helpers.device_registry.async_get_registry() + device_registry.async_get_or_create( + connection=[[CONNECTION_NETWORK_MAC, deconz.config.mac]], + identifiers=[[DOMAIN, deconz.config.bridgeid]], + manufacturer='Dresden Elektronik', model=deconz.config.modelid, + name=deconz.config.name, sw_version=deconz.config.swversion) + async def async_configure(call): """Set attribute of device in deCONZ. diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index e7bc5605aee..e629d57f201 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -8,6 +8,7 @@ CONFIG_FILE = 'deconz.conf' DATA_DECONZ_EVENT = 'deconz_events' DATA_DECONZ_ID = 'deconz_entities' DATA_DECONZ_UNSUB = 'deconz_dispatchers' +DECONZ_DOMAIN = 'deconz' CONF_ALLOW_CLIP_SENSOR = 'allow_clip_sensor' CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups' diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index 6dce6b7fdb8..067f1474f96 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -6,13 +6,14 @@ https://home-assistant.io/components/light.deconz/ """ from homeassistant.components.deconz.const import ( CONF_ALLOW_DECONZ_GROUPS, DOMAIN as DATA_DECONZ, - DATA_DECONZ_ID, DATA_DECONZ_UNSUB, SWITCH_TYPES) + DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DECONZ_DOMAIN, SWITCH_TYPES) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_TRANSITION, Light) from homeassistant.core import callback +from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util @@ -199,3 +200,19 @@ class DeconzLight(Light): if self._light.type == 'LightGroup': attributes['all_on'] = self._light.all_on return attributes + + @property + def device(self): + """Return a device description for device registry.""" + if (self._light.uniqueid is None or + self._light.uniqueid.count(':') != 7): + return None + serial = self._light.uniqueid.split('-', 1)[0] + return { + 'connection': [[CONNECTION_ZIGBEE, serial]], + 'identifiers': [[DECONZ_DOMAIN, serial]], + 'manufacturer': self._light.manufacturer, + 'model': self._light.modelid, + 'name': self._light.name, + 'sw_version': self._light.swversion, + } diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py index a32f1e5e210..45c604a74ee 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -6,10 +6,11 @@ https://home-assistant.io/components/sensor.deconz/ """ from homeassistant.components.deconz.const import ( ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ, - DATA_DECONZ_ID, DATA_DECONZ_UNSUB) + DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DECONZ_DOMAIN) from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY) from homeassistant.core import callback +from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.util import slugify @@ -134,6 +135,22 @@ class DeconzSensor(Entity): attr[ATTR_DAYLIGHT] = self._sensor.daylight return attr + @property + def device(self): + """Return a device description for device registry.""" + if (self._sensor.uniqueid is None or + self._sensor.uniqueid.count(':') != 7): + return None + serial = self._sensor.uniqueid.split('-', 1)[0] + return { + 'connection': [[CONNECTION_ZIGBEE, serial]], + 'identifiers': [[DECONZ_DOMAIN, serial]], + 'manufacturer': self._sensor.manufacturer, + 'model': self._sensor.modelid, + 'name': self._sensor.name, + 'sw_version': self._sensor.swversion, + } + class DeconzBattery(Entity): """Battery class for when a device is only represented as an event.""" @@ -192,3 +209,19 @@ class DeconzBattery(Entity): ATTR_EVENT_ID: slugify(self._device.name), } return attr + + @property + def device(self): + """Return a device description for device registry.""" + if (self._device.uniqueid is None or + self._device.uniqueid.count(':') != 7): + return None + serial = self._device.uniqueid.split('-', 1)[0] + return { + 'connection': [[CONNECTION_ZIGBEE, serial]], + 'identifiers': [[DECONZ_DOMAIN, serial]], + 'manufacturer': self._device.manufacturer, + 'model': self._device.modelid, + 'name': self._device.name, + 'sw_version': self._device.swversion, + } diff --git a/homeassistant/components/switch/deconz.py b/homeassistant/components/switch/deconz.py index 11f7f42c6c9..7d861e4c29c 100644 --- a/homeassistant/components/switch/deconz.py +++ b/homeassistant/components/switch/deconz.py @@ -6,9 +6,10 @@ https://home-assistant.io/components/switch.deconz/ """ from homeassistant.components.deconz.const import ( DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB, - POWER_PLUGS, SIRENS) + DECONZ_DOMAIN, POWER_PLUGS, SIRENS) from homeassistant.components.switch import SwitchDevice from homeassistant.core import callback +from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.dispatcher import async_dispatcher_connect DEPENDENCIES = ['deconz'] @@ -79,6 +80,22 @@ class DeconzSwitch(SwitchDevice): """No polling needed.""" return False + @property + def device(self): + """Return a device description for device registry.""" + if (self._switch.uniqueid is None or + self._switch.uniqueid.count(':') != 7): + return None + serial = self._switch.uniqueid.split('-', 1)[0] + return { + 'connection': [[CONNECTION_ZIGBEE, serial]], + 'identifiers': [[DECONZ_DOMAIN, serial]], + 'manufacturer': self._switch.manufacturer, + 'model': self._switch.modelid, + 'name': self._switch.name, + 'sw_version': self._switch.swversion, + } + class DeconzPowerPlug(DeconzSwitch): """Representation of power plugs from deCONZ.""" diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 3276763a967..19a6eaa62dc 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -15,15 +15,18 @@ STORAGE_KEY = 'core.device_registry' STORAGE_VERSION = 1 SAVE_DELAY = 10 +CONNECTION_NETWORK_MAC = 'mac' +CONNECTION_ZIGBEE = 'zigbee' + @attr.s(slots=True, frozen=True) class DeviceEntry: """Device Registry Entry.""" + connection = attr.ib(type=list) identifiers = attr.ib(type=list) manufacturer = attr.ib(type=str) model = attr.ib(type=str) - connection = attr.ib(type=list) name = attr.ib(type=str, default=None) sw_version = attr.ib(type=str, default=None) id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) @@ -48,8 +51,8 @@ class DeviceRegistry: return None @callback - def async_get_or_create(self, identifiers, manufacturer, model, - connection, *, name=None, sw_version=None): + def async_get_or_create(self, *, connection, identifiers, manufacturer, + model, name=None, sw_version=None): """Get device. Create if it doesn't exist.""" device = self.async_get_device(identifiers, connection) @@ -57,10 +60,10 @@ class DeviceRegistry: return device device = DeviceEntry( + connection=connection, identifiers=identifiers, manufacturer=manufacturer, model=model, - connection=connection, name=name, sw_version=sw_version ) @@ -93,10 +96,10 @@ class DeviceRegistry: data['devices'] = [ { 'id': entry.id, + 'connection': entry.connection, 'identifiers': entry.identifiers, 'manufacturer': entry.manufacturer, 'model': entry.model, - 'connection': entry.connection, 'name': entry.name, 'sw_version': entry.sw_version, } for entry in self.devices diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index c65aa5e98c2..ffac68c5f07 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -275,8 +275,11 @@ class EntityPlatform: device = entity.device if device is not None: device = device_registry.async_get_or_create( - device['identifiers'], device['manufacturer'], - device['model'], device['connection'], + connection=device['connection'], + identifiers=device['identifiers'], + manufacturer=device['manufacturer'], + model=device['model'], + name=device.get('name'), sw_version=device.get('sw_version')) device_id = device.id else: diff --git a/requirements_all.txt b/requirements_all.txt index 25480a023ec..447c6348500 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -810,7 +810,7 @@ pycsspeechtts==1.0.2 pydaikin==0.4 # homeassistant.components.deconz -pydeconz==43 +pydeconz==44 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 71cbc724c59..52688beaa26 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -139,7 +139,7 @@ py-canary==0.5.0 pyblackbird==0.5 # homeassistant.components.deconz -pydeconz==43 +pydeconz==44 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index c6fc130a4a4..049a3b961b6 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -7,6 +7,16 @@ from homeassistant.components import deconz from tests.common import mock_coro +CONFIG = { + "config": { + "bridgeid": "0123456789ABCDEF", + "mac": "12:34:56:78:90:ab", + "modelid": "deCONZ", + "name": "Phoscon", + "swversion": "2.05.35" + } +} + async def test_config_with_host_passed_to_config_entry(hass): """Test that configured options for a host are loaded via config entry.""" @@ -93,8 +103,11 @@ async def test_setup_entry_successful(hass): entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} with patch.object(hass, 'async_create_task') as mock_add_job, \ patch.object(hass, 'config_entries') as mock_config_entries, \ - patch('pydeconz.DeconzSession.async_load_parameters', - return_value=mock_coro(True)): + patch('pydeconz.DeconzSession.async_get_state', + return_value=mock_coro(CONFIG)), \ + patch('pydeconz.DeconzSession.start', return_value=True), \ + patch('homeassistant.helpers.device_registry.async_get_registry', + return_value=mock_coro(Mock())): assert await deconz.async_setup_entry(hass, entry) is True assert hass.data[deconz.DOMAIN] assert hass.data[deconz.DATA_DECONZ_ID] == {} @@ -117,10 +130,15 @@ async def test_unload_entry(hass): """Test being able to unload an entry.""" entry = Mock() entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} - with patch('pydeconz.DeconzSession.async_load_parameters', - return_value=mock_coro(True)): + entry.async_unload.return_value = mock_coro(True) + deconzmock = Mock() + deconzmock.async_load_parameters.return_value = mock_coro(True) + deconzmock.sensors = {} + with patch('pydeconz.DeconzSession', return_value=deconzmock): assert await deconz.async_setup_entry(hass, entry) is True + assert deconz.DATA_DECONZ_EVENT in hass.data + hass.data[deconz.DATA_DECONZ_EVENT].append(Mock()) hass.data[deconz.DATA_DECONZ_ID] = {'id': 'deconzid'} assert await deconz.async_unload_entry(hass, entry) @@ -132,6 +150,9 @@ async def test_unload_entry(hass): async def test_add_new_device(hass): """Test adding a new device generates a signal for platforms.""" + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, + 'api_key': '1234567890ABCDEF', 'allow_clip_sensor': False} new_event = { "t": "event", "e": "added", @@ -147,11 +168,10 @@ async def test_add_new_device(hass): "type": "ZHASwitch" } } - entry = Mock() - entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} with patch.object(deconz, 'async_dispatcher_send') as mock_dispatch_send, \ - patch('pydeconz.DeconzSession.async_load_parameters', - return_value=mock_coro(True)): + patch('pydeconz.DeconzSession.async_get_state', + return_value=mock_coro(CONFIG)), \ + patch('pydeconz.DeconzSession.start', return_value=True): assert await deconz.async_setup_entry(hass, entry) is True hass.data[deconz.DOMAIN].async_event_handler(new_event) await hass.async_block_till_done() @@ -162,15 +182,16 @@ async def test_add_new_device(hass): async def test_add_new_remote(hass): """Test new added device creates a new remote.""" entry = Mock() - entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + entry.data = {'host': '1.2.3.4', 'port': 80, + 'api_key': '1234567890ABCDEF', 'allow_clip_sensor': False} remote = Mock() remote.name = 'name' remote.type = 'ZHASwitch' remote.register_async_callback = Mock() - with patch('pydeconz.DeconzSession.async_load_parameters', - return_value=mock_coro(True)): + with patch('pydeconz.DeconzSession.async_get_state', + return_value=mock_coro(CONFIG)), \ + patch('pydeconz.DeconzSession.start', return_value=True): assert await deconz.async_setup_entry(hass, entry) is True - async_dispatcher_send(hass, 'deconz_new_sensor', [remote]) await hass.async_block_till_done() assert len(hass.data[deconz.DATA_DECONZ_EVENT]) == 1 @@ -185,8 +206,9 @@ async def test_do_not_allow_clip_sensor(hass): remote.name = 'name' remote.type = 'CLIPSwitch' remote.register_async_callback = Mock() - with patch('pydeconz.DeconzSession.async_load_parameters', - return_value=mock_coro(True)): + with patch('pydeconz.DeconzSession.async_get_state', + return_value=mock_coro(CONFIG)), \ + patch('pydeconz.DeconzSession.start', return_value=True): assert await deconz.async_setup_entry(hass, entry) is True async_dispatcher_send(hass, 'deconz_new_sensor', [remote]) diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 41e7d39e977..f7792eb5250 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -26,14 +26,17 @@ def registry(hass): async def test_get_or_create_returns_same_entry(registry): """Make sure we do not duplicate entries.""" entry = registry.async_get_or_create( - [['bridgeid', '0123']], 'manufacturer', 'model', - [['ethernet', '12:34:56:78:90:AB:CD:EF']]) + connection=[['ethernet', '12:34:56:78:90:AB:CD:EF']], + identifiers=[['bridgeid', '0123']], + manufacturer='manufacturer', model='model') entry2 = registry.async_get_or_create( - [['bridgeid', '0123']], 'manufacturer', 'model', - [['ethernet', '11:22:33:44:55:66:77:88']]) + connection=[['ethernet', '11:22:33:44:55:66:77:88']], + identifiers=[['bridgeid', '0123']], + manufacturer='manufacturer', model='model') entry3 = registry.async_get_or_create( - [['bridgeid', '1234']], 'manufacturer', 'model', - [['ethernet', '12:34:56:78:90:AB:CD:EF']]) + connection=[['ethernet', '12:34:56:78:90:AB:CD:EF']], + identifiers=[['bridgeid', '1234']], + manufacturer='manufacturer', model='model') assert len(registry.devices) == 1 assert entry is entry2 @@ -73,6 +76,7 @@ async def test_loading_from_storage(hass, hass_storage): registry = await device_registry.async_get_registry(hass) entry = registry.async_get_or_create( - [['serial', '12:34:56:78:90:AB:CD:EF']], 'manufacturer', - 'model', [['Zigbee', '01.23.45.67.89']]) + connection=[['Zigbee', '01.23.45.67.89']], + identifiers=[['serial', '12:34:56:78:90:AB:CD:EF']], + manufacturer='manufacturer', model='model') assert entry.id == 'abcdefghijklm' From 84365cde077d6a927947793bd5f8a4790f16c852 Mon Sep 17 00:00:00 2001 From: Nate Clark Date: Fri, 24 Aug 2018 17:27:12 -0400 Subject: [PATCH 003/172] fix error message for cv.matches_regex (#16175) --- homeassistant/helpers/config_validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index bbd863b5693..90098a677a1 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -92,7 +92,7 @@ def matches_regex(regex): if not regex.match(value): raise vol.Invalid('value {} does not match regular expression {}' - .format(regex.pattern, value)) + .format(value, regex.pattern)) return value return validator From 647b3ff0feea7ec5e48e8ae81f185d777ee4a15f Mon Sep 17 00:00:00 2001 From: Nate Clark Date: Fri, 24 Aug 2018 17:29:25 -0400 Subject: [PATCH 004/172] Decouple Konnected entity setup from discovery (#16146) * decouple entity setup from discovery * validate that device_id is a full MAC address --- homeassistant/components/konnected.py | 151 ++++++++++--------- homeassistant/components/switch/konnected.py | 17 ++- 2 files changed, 94 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/konnected.py b/homeassistant/components/konnected.py index 9e85e85818d..3df28586313 100644 --- a/homeassistant/components/konnected.py +++ b/homeassistant/components/konnected.py @@ -16,7 +16,7 @@ from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA from homeassistant.components.discovery import SERVICE_KONNECTED from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( - HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR, HTTP_UNAUTHORIZED, + HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_UNAUTHORIZED, CONF_DEVICES, CONF_BINARY_SENSORS, CONF_SWITCHES, CONF_HOST, CONF_PORT, CONF_ID, CONF_NAME, CONF_TYPE, CONF_PIN, CONF_ZONE, CONF_ACCESS_TOKEN, ATTR_ENTITY_ID, ATTR_STATE) @@ -74,7 +74,7 @@ CONFIG_SCHEMA = vol.Schema( vol.Required(CONF_ACCESS_TOKEN): cv.string, vol.Optional(CONF_API_HOST): vol.Url(), vol.Required(CONF_DEVICES): [{ - vol.Required(CONF_ID): cv.string, + vol.Required(CONF_ID): cv.matches_regex("[0-9a-f]{12}"), vol.Optional(CONF_BINARY_SENSORS): vol.All( cv.ensure_list, [_BINARY_SENSOR_SCHEMA]), vol.Optional(CONF_SWITCHES): vol.All( @@ -107,12 +107,18 @@ async def async_setup(hass, config): def device_discovered(service, info): """Call when a Konnected device has been discovered.""" - _LOGGER.debug("Discovered a new Konnected device: %s", info) host = info.get(CONF_HOST) port = info.get(CONF_PORT) + discovered = DiscoveredDevice(hass, host, port) + if discovered.is_configured: + discovered.setup() + else: + _LOGGER.warning("Konnected device %s was discovered on the network" + " but not specified in configuration.yaml", + discovered.device_id) - device = KonnectedDevice(hass, host, port, cfg) - device.setup() + for device in cfg.get(CONF_DEVICES): + ConfiguredDevice(hass, device).save_data() discovery.async_listen( hass, @@ -124,98 +130,51 @@ async def async_setup(hass, config): return True -class KonnectedDevice: - """A representation of a single Konnected device.""" +class ConfiguredDevice: + """A representation of a configured Konnected device.""" - def __init__(self, hass, host, port, config): + def __init__(self, hass, config): """Initialize the Konnected device.""" self.hass = hass - self.host = host - self.port = port - self.user_config = config - - import konnected - self.client = konnected.Client(host, str(port)) - self.status = self.client.get_status() - _LOGGER.info('Initialized Konnected device %s', self.device_id) - - def setup(self): - """Set up a newly discovered Konnected device.""" - user_config = self.config() - if user_config: - _LOGGER.debug('Configuring Konnected device %s', self.device_id) - self.save_data() - self.sync_device_config() - discovery.load_platform( - self.hass, 'binary_sensor', - DOMAIN, {'device_id': self.device_id}) - discovery.load_platform( - self.hass, 'switch', DOMAIN, - {'device_id': self.device_id}) + self.config = config @property def device_id(self): """Device id is the MAC address as string with punctuation removed.""" - return self.status['mac'].replace(':', '') - - def config(self): - """Return an object representing the user defined configuration.""" - device_id = self.device_id - valid_keys = [device_id, device_id.upper(), - device_id[6:], device_id.upper()[6:]] - configured_devices = self.user_config[CONF_DEVICES] - return next((device for device in - configured_devices if device[CONF_ID] in valid_keys), - None) + return self.config.get(CONF_ID) def save_data(self): """Save the device configuration to `hass.data`.""" sensors = {} - for entity in self.config().get(CONF_BINARY_SENSORS) or []: + for entity in self.config.get(CONF_BINARY_SENSORS) or []: if CONF_ZONE in entity: pin = ZONE_TO_PIN[entity[CONF_ZONE]] else: pin = entity[CONF_PIN] - sensor_status = next((sensor for sensor in - self.status.get('sensors') if - sensor.get(CONF_PIN) == pin), {}) - if sensor_status.get(ATTR_STATE): - initial_state = bool(int(sensor_status.get(ATTR_STATE))) - else: - initial_state = None - sensors[pin] = { CONF_TYPE: entity[CONF_TYPE], CONF_NAME: entity.get(CONF_NAME, 'Konnected {} Zone {}'.format( self.device_id[6:], PIN_TO_ZONE[pin])), - ATTR_STATE: initial_state + ATTR_STATE: None } _LOGGER.debug('Set up sensor %s (initial state: %s)', sensors[pin].get('name'), sensors[pin].get(ATTR_STATE)) actuators = [] - for entity in self.config().get(CONF_SWITCHES) or []: + for entity in self.config.get(CONF_SWITCHES) or []: if 'zone' in entity: pin = ZONE_TO_PIN[entity['zone']] else: pin = entity['pin'] - actuator_status = next((actuator for actuator in - self.status.get('actuators') if - actuator.get('pin') == pin), {}) - if actuator_status.get(ATTR_STATE): - initial_state = bool(int(actuator_status.get(ATTR_STATE))) - else: - initial_state = None - act = { CONF_PIN: pin, CONF_NAME: entity.get( CONF_NAME, 'Konnected {} Actuator {}'.format( self.device_id[6:], PIN_TO_ZONE[pin])), - ATTR_STATE: initial_state, + ATTR_STATE: None, CONF_ACTIVATION: entity[CONF_ACTIVATION], CONF_MOMENTARY: entity.get(CONF_MOMENTARY), CONF_PAUSE: entity.get(CONF_PAUSE), @@ -224,23 +183,67 @@ class KonnectedDevice: _LOGGER.debug('Set up actuator %s', act) device_data = { - 'client': self.client, CONF_BINARY_SENSORS: sensors, CONF_SWITCHES: actuators, - CONF_HOST: self.host, - CONF_PORT: self.port, } if CONF_DEVICES not in self.hass.data[DOMAIN]: self.hass.data[DOMAIN][CONF_DEVICES] = {} - _LOGGER.debug('Storing data in hass.data[konnected]: %s', device_data) + _LOGGER.debug('Storing data in hass.data[%s][%s][%s]: %s', + DOMAIN, CONF_DEVICES, self.device_id, device_data) self.hass.data[DOMAIN][CONF_DEVICES][self.device_id] = device_data + discovery.load_platform( + self.hass, 'binary_sensor', + DOMAIN, {'device_id': self.device_id}) + discovery.load_platform( + self.hass, 'switch', DOMAIN, + {'device_id': self.device_id}) + + +class DiscoveredDevice: + """A representation of a discovered Konnected device.""" + + def __init__(self, hass, host, port): + """Initialize the Konnected device.""" + self.hass = hass + self.host = host + self.port = port + + import konnected + self.client = konnected.Client(host, str(port)) + self.status = self.client.get_status() + + def setup(self): + """Set up a newly discovered Konnected device.""" + _LOGGER.info('Discovered Konnected device %s. Open http://%s:%s in a ' + 'web browser to view device status.', + self.device_id, self.host, self.port) + self.save_data() + self.update_initial_states() + self.sync_device_config() + + def save_data(self): + """Save the discovery information to `hass.data`.""" + self.stored_configuration['client'] = self.client + self.stored_configuration['host'] = self.host + self.stored_configuration['port'] = self.port + + @property + def device_id(self): + """Device id is the MAC address as string with punctuation removed.""" + return self.status['mac'].replace(':', '') + + @property + def is_configured(self): + """Return true if device_id is specified in the configuration.""" + return bool(self.hass.data[DOMAIN][CONF_DEVICES].get(self.device_id)) + @property def stored_configuration(self): """Return the configuration stored in `hass.data` for this device.""" - return self.hass.data[DOMAIN][CONF_DEVICES][self.device_id] + return self.hass.data[DOMAIN][CONF_DEVICES].get(self.device_id) def sensor_configuration(self): """Return the configuration map for syncing sensors.""" @@ -254,6 +257,18 @@ class KonnectedDevice: else 1)} for data in self.stored_configuration[CONF_SWITCHES]] + def update_initial_states(self): + """Update the initial state of each sensor from status poll.""" + for sensor in self.status.get('sensors'): + entity_id = self.stored_configuration[CONF_BINARY_SENSORS]. \ + get(sensor.get(CONF_PIN), {}). \ + get(ATTR_ENTITY_ID) + + async_dispatcher_send( + self.hass, + SIGNAL_SENSOR_UPDATE.format(entity_id), + bool(sensor.get(ATTR_STATE))) + def sync_device_config(self): """Sync the new pin configuration to the Konnected device.""" desired_sensor_configuration = self.sensor_configuration() @@ -285,7 +300,7 @@ class KonnectedDevice: if (desired_sensor_configuration != current_sensor_configuration) or \ (current_actuator_config != desired_actuator_config) or \ (current_api_endpoint != desired_api_endpoint): - _LOGGER.debug('pushing settings to device %s', self.device_id) + _LOGGER.info('pushing settings to device %s', self.device_id) self.client.put_settings( desired_sensor_configuration, desired_actuator_config, @@ -340,7 +355,7 @@ class KonnectedView(HomeAssistantView): entity_id = pin_data.get(ATTR_ENTITY_ID) if entity_id is None: return self.json_message('uninitialized sensor/actuator', - status_code=HTTP_INTERNAL_SERVER_ERROR) + status_code=HTTP_NOT_FOUND) async_dispatcher_send( hass, SIGNAL_SENSOR_UPDATE.format(entity_id), state) diff --git a/homeassistant/components/switch/konnected.py b/homeassistant/components/switch/konnected.py index c085d0bb0a5..20774accbd5 100644 --- a/homeassistant/components/switch/konnected.py +++ b/homeassistant/components/switch/konnected.py @@ -27,9 +27,8 @@ async def async_setup_platform(hass, config, async_add_entities, data = hass.data[KONNECTED_DOMAIN] device_id = discovery_info['device_id'] - client = data[CONF_DEVICES][device_id]['client'] switches = [ - KonnectedSwitch(device_id, pin_data.get(CONF_PIN), pin_data, client) + KonnectedSwitch(device_id, pin_data.get(CONF_PIN), pin_data) for pin_data in data[CONF_DEVICES][device_id][CONF_SWITCHES]] async_add_entities(switches) @@ -37,7 +36,7 @@ async def async_setup_platform(hass, config, async_add_entities, class KonnectedSwitch(ToggleEntity): """Representation of a Konnected switch.""" - def __init__(self, device_id, pin_num, data, client): + def __init__(self, device_id, pin_num, data): """Initialize the switch.""" self._data = data self._device_id = device_id @@ -50,7 +49,6 @@ class KonnectedSwitch(ToggleEntity): self._name = self._data.get( 'name', 'Konnected {} Actuator {}'.format( device_id, PIN_TO_ZONE[pin_num])) - self._client = client _LOGGER.debug('Created new switch: %s', self._name) @property @@ -63,9 +61,16 @@ class KonnectedSwitch(ToggleEntity): """Return the status of the sensor.""" return self._state + @property + def client(self): + """Return the Konnected HTTP client.""" + return \ + self.hass.data[KONNECTED_DOMAIN][CONF_DEVICES][self._device_id].\ + get('client') + def turn_on(self, **kwargs): """Send a command to turn on the switch.""" - resp = self._client.put_device( + resp = self.client.put_device( self._pin_num, int(self._activation == STATE_HIGH), self._momentary, @@ -82,7 +87,7 @@ class KonnectedSwitch(ToggleEntity): def turn_off(self, **kwargs): """Send a command to turn off the switch.""" - resp = self._client.put_device( + resp = self.client.put_device( self._pin_num, int(self._activation == STATE_LOW)) if resp.get(ATTR_STATE) is not None: From 69cea6001ffd93ae9aeecbc671ef5b00eb992c79 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 25 Aug 2018 01:05:53 +0200 Subject: [PATCH 005/172] Add 'moon_phase' to Dark Sky sensor (#16179) --- homeassistant/components/sensor/darksky.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sensor/darksky.py b/homeassistant/components/sensor/darksky.py index 7ce51454ee5..a6c602602f4 100644 --- a/homeassistant/components/sensor/darksky.py +++ b/homeassistant/components/sensor/darksky.py @@ -33,10 +33,12 @@ DEFAULT_LANGUAGE = 'en' DEFAULT_NAME = 'Dark Sky' -DEPRECATED_SENSOR_TYPES = {'apparent_temperature_max', - 'apparent_temperature_min', - 'temperature_max', - 'temperature_min'} +DEPRECATED_SENSOR_TYPES = { + 'apparent_temperature_max', + 'apparent_temperature_min', + 'temperature_max', + 'temperature_min', +} # Sensor types are defined like so: # Name, si unit, us unit, ca unit, uk unit, uk2 unit @@ -125,6 +127,8 @@ SENSOR_TYPES = { UNIT_UV_INDEX, UNIT_UV_INDEX, UNIT_UV_INDEX, UNIT_UV_INDEX, UNIT_UV_INDEX, 'mdi:weather-sunny', ['currently', 'hourly', 'daily']], + 'moon_phase': ['Moon Phase', None, None, None, None, None, + 'mdi:weather-night', ['daily']], } CONDITION_PICTURES = { @@ -203,7 +207,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensors = [] for variable in config[CONF_MONITORED_CONDITIONS]: if variable in DEPRECATED_SENSOR_TYPES: - _LOGGER.warning("Monitored condition %s is deprecated.", + _LOGGER.warning("Monitored condition %s is deprecated", variable) sensors.append(DarkSkySensor(forecast_data, variable, name)) if forecast is not None and 'daily' in SENSOR_TYPES[variable][7]: @@ -316,7 +320,8 @@ class DarkSkySensor(Entity): 'apparent_temperature_max', 'apparent_temperature_high', 'precip_intensity_max', - 'precip_accumulation']): + 'precip_accumulation', + 'moon_phase']): self.forecast_data.update_daily() daily = self.forecast_data.data_daily if self.type == 'daily_summary': @@ -407,7 +412,7 @@ class DarkSkyData: self._api_key, self.latitude, self.longitude, units=self.units, lang=self.language) except (ConnectError, HTTPError, Timeout, ValueError) as error: - _LOGGER.error("Unable to connect to Dark Sky. %s", error) + _LOGGER.error("Unable to connect to Dark Sky: %s", error) self.data = None self.unit_system = self.data and self.data.json['flags']['units'] From 24a8d60566fb5cf62942d042d38965a705d1bc65 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Fri, 24 Aug 2018 22:57:36 -0700 Subject: [PATCH 006/172] Tweak log level for bearer token warning (#16182) --- homeassistant/components/http/auth.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index d01d1b50c5a..7adcc43f4af 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -30,8 +30,10 @@ def setup_auth(app, trusted_networks, use_auth, if use_auth and (HTTP_HEADER_HA_AUTH in request.headers or DATA_API_PASSWORD in request.query): if request.path not in old_auth_warning: - _LOGGER.warning('Please change to use bearer token access %s', - request.path) + _LOGGER.log( + logging.INFO if support_legacy else logging.WARNING, + 'Please change to use bearer token access %s from %s', + request.path, request[KEY_REAL_IP]) old_auth_warning.add(request.path) legacy_auth = (not use_auth or support_legacy) and api_password From 97173f495c78b7add5e747857d988969be0e1ae6 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 25 Aug 2018 10:59:28 +0200 Subject: [PATCH 007/172] Device registry store config entry (#16152) * Allow device registry to optionally store config entries * Connections and identifiers are now sets with tupels * Make config entries mandatory * Fix duplicate keys in test * Rename device to device_info * Entity platform should only create device entries if config_entry_id exists * Fix Soundtouch tests * Revert soundtouch to use self.device * Fix baloobs comments * Correct type in test --- .../components/binary_sensor/deconz.py | 6 +- homeassistant/components/deconz/__init__.py | 5 +- homeassistant/components/light/deconz.py | 6 +- homeassistant/components/media_player/roku.py | 12 +-- .../components/media_player/soundtouch.py | 17 ++-- homeassistant/components/sensor/deconz.py | 12 +-- homeassistant/components/switch/deconz.py | 6 +- homeassistant/helpers/device_registry.py | 42 +++++++--- homeassistant/helpers/entity.py | 2 +- homeassistant/helpers/entity_platform.py | 17 ++-- tests/helpers/test_device_registry.py | 78 ++++++++++++++++--- 11 files changed, 142 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py index 9aa0c446f2b..1fb62124407 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -116,15 +116,15 @@ class DeconzBinarySensor(BinarySensorDevice): return attr @property - def device(self): + def device_info(self): """Return a device description for device registry.""" if (self._sensor.uniqueid is None or self._sensor.uniqueid.count(':') != 7): return None serial = self._sensor.uniqueid.split('-', 1)[0] return { - 'connection': [[CONNECTION_ZIGBEE, serial]], - 'identifiers': [[DECONZ_DOMAIN, serial]], + 'connections': {(CONNECTION_ZIGBEE, serial)}, + 'identifiers': {(DECONZ_DOMAIN, serial)}, 'manufacturer': self._sensor.manufacturer, 'model': self._sensor.modelid, 'name': self._sensor.name, diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index d435e9e3c04..a4edc009ea1 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -123,8 +123,9 @@ async def async_setup_entry(hass, config_entry): device_registry = await \ hass.helpers.device_registry.async_get_registry() device_registry.async_get_or_create( - connection=[[CONNECTION_NETWORK_MAC, deconz.config.mac]], - identifiers=[[DOMAIN, deconz.config.bridgeid]], + config_entry=config_entry.entry_id, + connections={(CONNECTION_NETWORK_MAC, deconz.config.mac)}, + identifiers={(DOMAIN, deconz.config.bridgeid)}, manufacturer='Dresden Elektronik', model=deconz.config.modelid, name=deconz.config.name, sw_version=deconz.config.swversion) diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index 067f1474f96..412cf8693e5 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -202,15 +202,15 @@ class DeconzLight(Light): return attributes @property - def device(self): + def device_info(self): """Return a device description for device registry.""" if (self._light.uniqueid is None or self._light.uniqueid.count(':') != 7): return None serial = self._light.uniqueid.split('-', 1)[0] return { - 'connection': [[CONNECTION_ZIGBEE, serial]], - 'identifiers': [[DECONZ_DOMAIN, serial]], + 'connections': {(CONNECTION_ZIGBEE, serial)}, + 'identifiers': {(DECONZ_DOMAIN, serial)}, 'manufacturer': self._light.manufacturer, 'model': self._light.modelid, 'name': self._light.name, diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/media_player/roku.py index fa1120db98c..fca7b29d2ec 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -87,7 +87,7 @@ class RokuDevice(MediaPlayerDevice): self.ip_address = host self.channels = [] self.current_app = None - self.device_info = {} + self._device_info = {} self.update() @@ -96,7 +96,7 @@ class RokuDevice(MediaPlayerDevice): import requests.exceptions try: - self.device_info = self.roku.device_info + self._device_info = self.roku.device_info self.ip_address = self.roku.host self.channels = self.get_source_list() @@ -121,9 +121,9 @@ class RokuDevice(MediaPlayerDevice): @property def name(self): """Return the name of the device.""" - if self.device_info.userdevicename: - return self.device_info.userdevicename - return "Roku {}".format(self.device_info.sernum) + if self._device_info.userdevicename: + return self._device_info.userdevicename + return "Roku {}".format(self._device_info.sernum) @property def state(self): @@ -149,7 +149,7 @@ class RokuDevice(MediaPlayerDevice): @property def unique_id(self): """Return a unique, HASS-friendly identifier for this entity.""" - return self.device_info.sernum + return self._device_info.sernum @property def media_content_type(self): diff --git a/homeassistant/components/media_player/soundtouch.py b/homeassistant/components/media_player/soundtouch.py index f2ac45a996f..489d028aad4 100644 --- a/homeassistant/components/media_player/soundtouch.py +++ b/homeassistant/components/media_player/soundtouch.py @@ -166,6 +166,11 @@ class SoundTouchDevice(MediaPlayerDevice): """Return specific soundtouch configuration.""" return self._config + @property + def device(self): + """Return Soundtouch device.""" + return self._device + def update(self): """Retrieve the latest data.""" self._status = self._device.status() @@ -318,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): """ @@ -336,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): """ @@ -352,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]) diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py index 45c604a74ee..8cb3915dc46 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -136,15 +136,15 @@ class DeconzSensor(Entity): return attr @property - def device(self): + def device_info(self): """Return a device description for device registry.""" if (self._sensor.uniqueid is None or self._sensor.uniqueid.count(':') != 7): return None serial = self._sensor.uniqueid.split('-', 1)[0] return { - 'connection': [[CONNECTION_ZIGBEE, serial]], - 'identifiers': [[DECONZ_DOMAIN, serial]], + 'connections': {(CONNECTION_ZIGBEE, serial)}, + 'identifiers': {(DECONZ_DOMAIN, serial)}, 'manufacturer': self._sensor.manufacturer, 'model': self._sensor.modelid, 'name': self._sensor.name, @@ -211,15 +211,15 @@ class DeconzBattery(Entity): return attr @property - def device(self): + def device_info(self): """Return a device description for device registry.""" if (self._device.uniqueid is None or self._device.uniqueid.count(':') != 7): return None serial = self._device.uniqueid.split('-', 1)[0] return { - 'connection': [[CONNECTION_ZIGBEE, serial]], - 'identifiers': [[DECONZ_DOMAIN, serial]], + 'connections': {(CONNECTION_ZIGBEE, serial)}, + 'identifiers': {(DECONZ_DOMAIN, serial)}, 'manufacturer': self._device.manufacturer, 'model': self._device.modelid, 'name': self._device.name, diff --git a/homeassistant/components/switch/deconz.py b/homeassistant/components/switch/deconz.py index 7d861e4c29c..35dbc3ef782 100644 --- a/homeassistant/components/switch/deconz.py +++ b/homeassistant/components/switch/deconz.py @@ -81,15 +81,15 @@ class DeconzSwitch(SwitchDevice): return False @property - def device(self): + def device_info(self): """Return a device description for device registry.""" if (self._switch.uniqueid is None or self._switch.uniqueid.count(':') != 7): return None serial = self._switch.uniqueid.split('-', 1)[0] return { - 'connection': [[CONNECTION_ZIGBEE, serial]], - 'identifiers': [[DECONZ_DOMAIN, serial]], + 'connections': {(CONNECTION_ZIGBEE, serial)}, + 'identifiers': {(DECONZ_DOMAIN, serial)}, 'manufacturer': self._switch.manufacturer, 'model': self._switch.modelid, 'name': self._switch.name, diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 19a6eaa62dc..31da40134a5 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -23,8 +23,9 @@ CONNECTION_ZIGBEE = 'zigbee' class DeviceEntry: """Device Registry Entry.""" - connection = attr.ib(type=list) - identifiers = attr.ib(type=list) + config_entries = attr.ib(type=set, converter=set) + connections = attr.ib(type=set, converter=set) + identifiers = attr.ib(type=set, converter=set) manufacturer = attr.ib(type=str) model = attr.ib(type=str) name = attr.ib(type=str, default=None) @@ -46,29 +47,36 @@ class DeviceRegistry: """Check if device is registered.""" for device in self.devices: if any(iden in device.identifiers for iden in identifiers) or \ - any(conn in device.connection for conn in connections): + any(conn in device.connections for conn in connections): return device return None @callback - def async_get_or_create(self, *, connection, identifiers, manufacturer, - model, name=None, sw_version=None): + def async_get_or_create(self, *, config_entry, connections, identifiers, + manufacturer, model, name=None, sw_version=None): """Get device. Create if it doesn't exist.""" - device = self.async_get_device(identifiers, connection) + if not identifiers and not connections: + return None + + device = self.async_get_device(identifiers, connections) if device is not None: + if config_entry not in device.config_entries: + device.config_entries.add(config_entry) + self.async_schedule_save() return device device = DeviceEntry( - connection=connection, + config_entries=[config_entry], + connections=connections, identifiers=identifiers, manufacturer=manufacturer, model=model, name=name, sw_version=sw_version ) - self.devices.append(device) + self.async_schedule_save() return device @@ -81,7 +89,16 @@ class DeviceRegistry: self.devices = [] return - self.devices = [DeviceEntry(**device) for device in devices['devices']] + self.devices = [DeviceEntry( + config_entries=device['config_entries'], + connections={tuple(conn) for conn in device['connections']}, + identifiers={tuple(iden) for iden in device['identifiers']}, + manufacturer=device['manufacturer'], + model=device['model'], + name=device['name'], + sw_version=device['sw_version'], + id=device['id'], + ) for device in devices['devices']] @callback def async_schedule_save(self): @@ -95,13 +112,14 @@ class DeviceRegistry: data['devices'] = [ { - 'id': entry.id, - 'connection': entry.connection, - 'identifiers': entry.identifiers, + 'config_entries': list(entry.config_entries), + 'connections': list(entry.connections), + 'identifiers': list(entry.identifiers), 'manufacturer': entry.manufacturer, 'model': entry.model, 'name': entry.name, 'sw_version': entry.sw_version, + 'id': entry.id, } for entry in self.devices ] diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 78806e65ef1..695da5bce9c 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -131,7 +131,7 @@ class Entity: return None @property - def device(self): + def device_info(self): """Return device specific attributes. Implemented by platform classes. diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index ffac68c5f07..083a2946122 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -272,15 +272,16 @@ class EntityPlatform: else: config_entry_id = None - device = entity.device - if device is not None: + device_info = entity.device_info + if config_entry_id is not None and device_info is not None: device = device_registry.async_get_or_create( - connection=device['connection'], - identifiers=device['identifiers'], - manufacturer=device['manufacturer'], - model=device['model'], - name=device.get('name'), - sw_version=device.get('sw_version')) + config_entry=config_entry_id, + connections=device_info.get('connections', []), + identifiers=device_info.get('identifiers', []), + manufacturer=device_info.get('manufacturer'), + model=device_info.get('model'), + name=device_info.get('name'), + sw_version=device_info.get('sw_version')) device_id = device.id else: device_id = None diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index f7792eb5250..b2e73071823 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -26,22 +26,73 @@ def registry(hass): async def test_get_or_create_returns_same_entry(registry): """Make sure we do not duplicate entries.""" entry = registry.async_get_or_create( - connection=[['ethernet', '12:34:56:78:90:AB:CD:EF']], - identifiers=[['bridgeid', '0123']], + config_entry='1234', + connections={('ethernet', '12:34:56:78:90:AB:CD:EF')}, + identifiers={('bridgeid', '0123')}, manufacturer='manufacturer', model='model') entry2 = registry.async_get_or_create( - connection=[['ethernet', '11:22:33:44:55:66:77:88']], - identifiers=[['bridgeid', '0123']], + config_entry='1234', + connections={('ethernet', '11:22:33:44:55:66:77:88')}, + identifiers={('bridgeid', '0123')}, manufacturer='manufacturer', model='model') entry3 = registry.async_get_or_create( - connection=[['ethernet', '12:34:56:78:90:AB:CD:EF']], - identifiers=[['bridgeid', '1234']], + config_entry='1234', + connections={('ethernet', '12:34:56:78:90:AB:CD:EF')}, + identifiers={('bridgeid', '1234')}, manufacturer='manufacturer', model='model') assert len(registry.devices) == 1 assert entry is entry2 assert entry is entry3 - assert entry.identifiers == [['bridgeid', '0123']] + assert entry.identifiers == {('bridgeid', '0123')} + + +async def test_requirement_for_identifier_or_connection(registry): + """Make sure we do require some descriptor of device.""" + entry = registry.async_get_or_create( + config_entry='1234', + connections={('ethernet', '12:34:56:78:90:AB:CD:EF')}, + identifiers=set(), + manufacturer='manufacturer', model='model') + entry2 = registry.async_get_or_create( + config_entry='1234', + connections=set(), + identifiers={('bridgeid', '0123')}, + manufacturer='manufacturer', model='model') + entry3 = registry.async_get_or_create( + config_entry='1234', + connections=set(), + identifiers=set(), + manufacturer='manufacturer', model='model') + + assert len(registry.devices) == 2 + assert entry + assert entry2 + assert entry3 is None + + +async def test_multiple_config_entries(registry): + """Make sure we do not get duplicate entries.""" + entry = registry.async_get_or_create( + config_entry='123', + connections={('ethernet', '12:34:56:78:90:AB:CD:EF')}, + identifiers={('bridgeid', '0123')}, + manufacturer='manufacturer', model='model') + entry2 = registry.async_get_or_create( + config_entry='456', + connections={('ethernet', '12:34:56:78:90:AB:CD:EF')}, + identifiers={('bridgeid', '0123')}, + manufacturer='manufacturer', model='model') + entry3 = registry.async_get_or_create( + config_entry='123', + connections={('ethernet', '12:34:56:78:90:AB:CD:EF')}, + identifiers={('bridgeid', '0123')}, + manufacturer='manufacturer', model='model') + + assert len(registry.devices) == 1 + assert entry is entry2 + assert entry is entry3 + assert entry.config_entries == {'123', '456'} async def test_loading_from_storage(hass, hass_storage): @@ -51,7 +102,10 @@ async def test_loading_from_storage(hass, hass_storage): 'data': { 'devices': [ { - 'connection': [ + 'config_entries': [ + '1234' + ], + 'connections': [ [ 'Zigbee', '01.23.45.67.89' @@ -67,7 +121,7 @@ async def test_loading_from_storage(hass, hass_storage): 'manufacturer': 'manufacturer', 'model': 'model', 'name': 'name', - 'sw_version': 'version' + 'sw_version': 'version', } ] } @@ -76,7 +130,9 @@ async def test_loading_from_storage(hass, hass_storage): registry = await device_registry.async_get_registry(hass) entry = registry.async_get_or_create( - connection=[['Zigbee', '01.23.45.67.89']], - identifiers=[['serial', '12:34:56:78:90:AB:CD:EF']], + config_entry='1234', + connections={('Zigbee', '01.23.45.67.89')}, + identifiers={('serial', '12:34:56:78:90:AB:CD:EF')}, manufacturer='manufacturer', model='model') assert entry.id == 'abcdefghijklm' + assert isinstance(entry.config_entries, set) From 456aa5a2b227ee4553b6d302a8a194fccff1c5f1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 25 Aug 2018 11:01:32 +0200 Subject: [PATCH 008/172] Fix hangouts (#16180) --- homeassistant/components/hangouts/__init__.py | 4 ++-- homeassistant/components/hangouts/config_flow.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hangouts/__init__.py b/homeassistant/components/hangouts/__init__.py index 89649ecb8e1..8ebacc3736b 100644 --- a/homeassistant/components/hangouts/__init__.py +++ b/homeassistant/components/hangouts/__init__.py @@ -26,8 +26,8 @@ _LOGGER = logging.getLogger(__name__) async def async_setup(hass, config): """Set up the Hangouts bot component.""" - config = config.get(DOMAIN, []) - hass.data[DOMAIN] = {CONF_COMMANDS: config[CONF_COMMANDS]} + config = config.get(DOMAIN, {}) + hass.data[DOMAIN] = {CONF_COMMANDS: config.get(CONF_COMMANDS, [])} if configured_hangouts(hass) is None: hass.async_add_job(hass.config_entries.flow.async_init( diff --git a/homeassistant/components/hangouts/config_flow.py b/homeassistant/components/hangouts/config_flow.py index bd81d5053c8..74eb14b050d 100644 --- a/homeassistant/components/hangouts/config_flow.py +++ b/homeassistant/components/hangouts/config_flow.py @@ -104,4 +104,4 @@ class HangoutsFlowHandler(data_entry_flow.FlowHandler): async def async_step_import(self, _): """Handle a flow import.""" - return self.async_abort(reason='already_configured') + return await self.async_step_user() From 26a485d43c8ba2bafa2e989ab65524fc427416ad Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Sat, 25 Aug 2018 02:09:48 -0700 Subject: [PATCH 009/172] Default load trusted_network auth provider if configured trusted networks (#16184) --- homeassistant/bootstrap.py | 4 +++- homeassistant/config.py | 5 ++++- tests/test_config.py | 21 +++++++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 41fa61964de..c10964e2da3 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -88,10 +88,12 @@ async def async_from_config_dict(config: Dict[str, Any], core_config = config.get(core.DOMAIN, {}) has_api_password = bool((config.get('http') or {}).get('api_password')) + has_trusted_networks = bool((config.get('http') or {}) + .get('trusted_networks')) try: await conf_util.async_process_ha_core_config( - hass, core_config, has_api_password) + hass, core_config, has_api_password, has_trusted_networks) except vol.Invalid as ex: conf_util.async_log_exception(ex, 'homeassistant', core_config, hass) return None diff --git a/homeassistant/config.py b/homeassistant/config.py index 45505bbbc9b..fe8f8ef0f60 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -406,7 +406,8 @@ def _format_config_error(ex: vol.Invalid, domain: str, config: Dict) -> str: async def async_process_ha_core_config( hass: HomeAssistant, config: Dict, - has_api_password: bool = False) -> None: + has_api_password: bool = False, + has_trusted_networks: bool = False) -> None: """Process the [homeassistant] section from the configuration. This method is a coroutine. @@ -423,6 +424,8 @@ async def async_process_ha_core_config( ] if has_api_password: auth_conf.append({'type': 'legacy_api_password'}) + if has_trusted_networks: + auth_conf.append({'type': 'trusted_networks'}) setattr(hass, 'auth', await auth.auth_manager_from_config( hass, diff --git a/tests/test_config.py b/tests/test_config.py index 77a30fd771b..76ea576ac28 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -856,6 +856,27 @@ async def test_auth_provider_config_default_api_password(hass): assert hass.auth.active is True +async def test_auth_provider_config_default_trusted_networks(hass): + """Test loading default auth provider config with trusted networks.""" + core_config = { + 'latitude': 60, + 'longitude': 50, + 'elevation': 25, + 'name': 'Huis', + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, + 'time_zone': 'GMT', + } + if hasattr(hass, 'auth'): + del hass.auth + await config_util.async_process_ha_core_config(hass, core_config, + has_trusted_networks=True) + + assert len(hass.auth.auth_providers) == 2 + assert hass.auth.auth_providers[0].type == 'homeassistant' + assert hass.auth.auth_providers[1].type == 'trusted_networks' + assert hass.auth.active is True + + async def test_disallowed_auth_provider_config(hass): """Test loading insecure example auth provider is disallowed.""" core_config = { From 617802653fe44de57fb4dd41d21955d5dfd50427 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 25 Aug 2018 11:15:01 +0200 Subject: [PATCH 010/172] Bump frontend to 20180825.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index bfcf7322749..c475ea55974 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -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==20180824.0'] +REQUIREMENTS = ['home-assistant-frontend==20180825.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 447c6348500..a5cfe9e402a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -438,7 +438,7 @@ hole==0.3.0 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180824.0 +home-assistant-frontend==20180825.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 52688beaa26..7b9dc1d1eb3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -84,7 +84,7 @@ hbmqtt==0.9.2 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180824.0 +home-assistant-frontend==20180825.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From f929c38e980d728bc74055043310bdc4c24211a1 Mon Sep 17 00:00:00 2001 From: djm300 Date: Sat, 25 Aug 2018 11:21:57 +0200 Subject: [PATCH 011/172] Zoneminder SSL fix (#16157) * Update zoneminder.py Added a verify_ssl parameter for zoneminder * PEP8 fixup * PEP8 indenting fix * Fix lint issue * Remove whitespace --- homeassistant/components/zoneminder.py | 28 +++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zoneminder.py b/homeassistant/components/zoneminder.py index 471c1c6e82c..5c045544456 100644 --- a/homeassistant/components/zoneminder.py +++ b/homeassistant/components/zoneminder.py @@ -11,16 +11,19 @@ import requests import voluptuous as vol from homeassistant.const import ( - CONF_PATH, CONF_HOST, CONF_SSL, CONF_PASSWORD, CONF_USERNAME) + CONF_HOST, CONF_PASSWORD, CONF_PATH, CONF_SSL, CONF_USERNAME, + CONF_VERIFY_SSL) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) CONF_PATH_ZMS = 'path_zms' + DEFAULT_PATH = '/zm/' DEFAULT_PATH_ZMS = '/zm/cgi-bin/nph-zms' DEFAULT_SSL = False DEFAULT_TIMEOUT = 10 +DEFAULT_VERIFY_SSL = True DOMAIN = 'zoneminder' LOGIN_RETRIES = 2 @@ -30,12 +33,12 @@ ZM = {} CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string, - # This should match PATH_ZMS in ZoneMinder settings. vol.Optional(CONF_PATH_ZMS, default=DEFAULT_PATH_ZMS): cv.string, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, }) }, extra=vol.ALLOW_EXTRA) @@ -56,11 +59,14 @@ def setup(hass, config): username = conf.get(CONF_USERNAME, None) password = conf.get(CONF_PASSWORD, None) + ssl_verification = conf.get(CONF_VERIFY_SSL) + ZM['server_origin'] = server_origin ZM['url'] = url ZM['username'] = username ZM['password'] = password ZM['path_zms'] = conf.get(CONF_PATH_ZMS) + ZM['ssl_verification'] = ssl_verification hass.data[DOMAIN] = ZM @@ -77,14 +83,16 @@ def login(): if ZM['password']: login_post['password'] = ZM['password'] - req = requests.post(ZM['url'] + '/index.php', data=login_post) + req = requests.post(ZM['url'] + '/index.php', data=login_post, + verify=ZM['ssl_verification'], timeout=DEFAULT_TIMEOUT) + ZM['cookies'] = req.cookies # Login calls returns a 200 response on both failure and success. # The only way to tell if you logged in correctly is to issue an api call. req = requests.get( ZM['url'] + 'api/host/getVersion.json', cookies=ZM['cookies'], - timeout=DEFAULT_TIMEOUT) + timeout=DEFAULT_TIMEOUT, verify=ZM['ssl_verification']) if not req.ok: _LOGGER.error("Connection error logging into ZoneMinder") @@ -100,7 +108,8 @@ def _zm_request(method, api_url, data=None): for _ in range(LOGIN_RETRIES): req = requests.request( method, urljoin(ZM['url'], api_url), data=data, - cookies=ZM['cookies'], timeout=DEFAULT_TIMEOUT) + cookies=ZM['cookies'], timeout=DEFAULT_TIMEOUT, + verify=ZM['ssl_verification']) if not req.ok: login() @@ -113,8 +122,9 @@ def _zm_request(method, api_url, data=None): try: return req.json() except ValueError: - _LOGGER.exception('JSON decode exception caught while attempting to ' - 'decode "%s"', req.text) + _LOGGER.exception( + "JSON decode exception caught while attempting to decode: %s", + req.text) def get_state(api_url): From 2f2bcf0058e9ce2e56cc06e8cf6786b44fa6c866 Mon Sep 17 00:00:00 2001 From: Thomas Delaet Date: Sat, 25 Aug 2018 20:42:26 +0200 Subject: [PATCH 012/172] update python-velbus library version (#16194) --- homeassistant/components/velbus.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/velbus.py b/homeassistant/components/velbus.py index 8c944916905..a6cdcc7cf90 100644 --- a/homeassistant/components/velbus.py +++ b/homeassistant/components/velbus.py @@ -12,7 +12,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_PORT from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['python-velbus==2.0.17'] +REQUIREMENTS = ['python-velbus==2.0.18'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index a5cfe9e402a..c150ac482bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1134,7 +1134,7 @@ python-telegram-bot==10.1.0 python-twitch==1.3.0 # homeassistant.components.velbus -python-velbus==2.0.17 +python-velbus==2.0.18 # homeassistant.components.media_player.vlc python-vlc==1.1.2 From a1ce14e70f4061873d471acce9aff03f82a67a51 Mon Sep 17 00:00:00 2001 From: Matthias Urlichs Date: Sun, 26 Aug 2018 10:04:51 +0200 Subject: [PATCH 013/172] MQTT: Log transmitted as well as received messages (#16195) --- homeassistant/components/mqtt/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 19bacbc8d4c..71be9c2435e 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -550,6 +550,7 @@ class MQTT: This method must be run in the event loop and returns a coroutine. """ async with self._paho_lock: + _LOGGER.debug("Transmitting message on %s: %s", topic, payload) await self.hass.async_add_job( self._mqttc.publish, topic, payload, qos, retain) From 0a7055d47551944f1319a0302302db60c9589879 Mon Sep 17 00:00:00 2001 From: Dan Klaffenbach Date: Sun, 26 Aug 2018 12:00:20 +0200 Subject: [PATCH 014/172] homematic: Make device avilable again when UNREACH becomes False (#16202) --- homeassistant/components/homematic/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 527b8c8f018..53c8e267016 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -869,7 +869,7 @@ class HMDevice(Entity): # Availability has changed if attribute == 'UNREACH': - self._available = bool(value) + self._available = not bool(value) has_changed = True elif not self.available: self._available = False From 0da3e737651a150c17016f43b5f9144deff7ddd7 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 26 Aug 2018 12:28:44 +0200 Subject: [PATCH 015/172] Upgrade sqlalchemy to 1.2.11 (#16192) --- homeassistant/components/recorder/__init__.py | 2 +- homeassistant/components/sensor/sql.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index f3d8e269a42..47d6e181c8f 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -35,7 +35,7 @@ from . import migration, purge from .const import DATA_INSTANCE from .util import session_scope -REQUIREMENTS = ['sqlalchemy==1.2.10'] +REQUIREMENTS = ['sqlalchemy==1.2.11'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/sql.py b/homeassistant/components/sensor/sql.py index a2e9549a117..53821275d42 100644 --- a/homeassistant/components/sensor/sql.py +++ b/homeassistant/components/sensor/sql.py @@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['sqlalchemy==1.2.10'] +REQUIREMENTS = ['sqlalchemy==1.2.11'] CONF_COLUMN_NAME = 'column' CONF_QUERIES = 'queries' @@ -62,7 +62,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): engine = sqlalchemy.create_engine(db_url) sessionmaker = scoped_session(sessionmaker(bind=engine)) - # run a dummy query just to test the db_url + # Run a dummy query just to test the db_url sess = sessionmaker() sess.execute("SELECT 1;") diff --git a/requirements_all.txt b/requirements_all.txt index c150ac482bf..41d716c28f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1334,7 +1334,7 @@ spotipy-homeassistant==2.4.4.dev1 # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql -sqlalchemy==1.2.10 +sqlalchemy==1.2.11 # homeassistant.components.statsd statsd==3.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7b9dc1d1eb3..af68edbd632 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -200,7 +200,7 @@ somecomfort==0.5.2 # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql -sqlalchemy==1.2.10 +sqlalchemy==1.2.11 # homeassistant.components.statsd statsd==3.2.1 From 289b1802fdd7670bfa563cdf3062d1186ab4f942 Mon Sep 17 00:00:00 2001 From: Martin Fuchs <39280548+fucm@users.noreply.github.com> Date: Sun, 26 Aug 2018 21:20:34 +0200 Subject: [PATCH 016/172] Add battery warning, rssi level and check for availability (#16193) --- homeassistant/components/sensor/tahoma.py | 32 ++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/tahoma.py b/homeassistant/components/sensor/tahoma.py index eafc6fdf616..a59eb705498 100644 --- a/homeassistant/components/sensor/tahoma.py +++ b/homeassistant/components/sensor/tahoma.py @@ -11,12 +11,15 @@ from datetime import timedelta from homeassistant.helpers.entity import Entity from homeassistant.components.tahoma import ( DOMAIN as TAHOMA_DOMAIN, TahomaDevice) +from homeassistant.const import ATTR_BATTERY_LEVEL DEPENDENCIES = ['tahoma'] _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=10) +SCAN_INTERVAL = timedelta(seconds=60) + +ATTR_RSSI_LEVEL = 'rssi_level' def setup_platform(hass, config, add_entities, discovery_info=None): @@ -34,6 +37,7 @@ class TahomaSensor(TahomaDevice, Entity): def __init__(self, tahoma_device, controller): """Initialize the sensor.""" self.current_value = None + self._available = False super().__init__(tahoma_device, controller) @property @@ -62,3 +66,29 @@ class TahomaSensor(TahomaDevice, Entity): if self.tahoma_device.type == 'io:SomfyContactIOSystemSensor': self.current_value = self.tahoma_device.active_states[ 'core:ContactState'] + + self._available = bool(self.tahoma_device.active_states.get( + 'core:StatusState') == 'available') + + _LOGGER.debug("Update %s, value: %d", self._name, self.current_value) + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attr = {} + super_attr = super().device_state_attributes + if super_attr is not None: + attr.update(super_attr) + + if 'core:RSSILevelState' in self.tahoma_device.active_states: + attr[ATTR_RSSI_LEVEL] = \ + self.tahoma_device.active_states['core:RSSILevelState'] + if 'core:SensorDefectState' in self.tahoma_device.active_states: + attr[ATTR_BATTERY_LEVEL] = \ + self.tahoma_device.active_states['core:SensorDefectState'] + return attr + + @property + def available(self): + """Return True if entity is available.""" + return self._available From 5341785aaec8471599ea4110c5c6d2271d4dacdd Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 26 Aug 2018 21:25:39 +0200 Subject: [PATCH 017/172] Revert changes to platforms using self.device (#16209) * Revert tank_utility * Fix Soundtouch * Fix Plex * Fix Emby * Fix Radiotherm * Fix Juicenet * Fix Qwikswitch * Fix Xiaomi miio * Fix Nest * Fix Tellduslive * Fix KNX --- homeassistant/components/binary_sensor/knx.py | 10 +-- .../components/binary_sensor/nest.py | 5 +- .../components/binary_sensor/tellduslive.py | 2 +- homeassistant/components/camera/nest.py | 18 +++--- homeassistant/components/climate/knx.py | 30 ++++----- homeassistant/components/climate/nest.py | 44 ++++++------- .../components/climate/radiotherm.py | 32 +++++----- homeassistant/components/cover/knx.py | 34 +++++----- homeassistant/components/cover/tellduslive.py | 8 +-- homeassistant/components/juicenet.py | 12 ++-- homeassistant/components/light/knx.py | 32 +++++----- homeassistant/components/light/qwikswitch.py | 4 +- homeassistant/components/light/tellduslive.py | 8 +-- homeassistant/components/media_player/emby.py | 52 +++++++-------- homeassistant/components/media_player/plex.py | 63 ++++++++++--------- .../components/media_player/soundtouch.py | 12 ++-- homeassistant/components/nest/__init__.py | 6 +- homeassistant/components/notify/knx.py | 8 +-- homeassistant/components/qwikswitch.py | 6 +- .../components/remote/xiaomi_miio.py | 9 ++- homeassistant/components/sensor/juicenet.py | 20 +++--- homeassistant/components/sensor/knx.py | 10 +-- homeassistant/components/sensor/nest.py | 10 +-- .../components/sensor/tank_utility.py | 11 +++- .../components/sensor/tellduslive.py | 2 +- homeassistant/components/switch/knx.py | 12 ++-- .../components/switch/tellduslive.py | 6 +- homeassistant/components/tellduslive.py | 27 ++++---- 28 files changed, 256 insertions(+), 237 deletions(-) diff --git a/homeassistant/components/binary_sensor/knx.py b/homeassistant/components/binary_sensor/knx.py index a7d1d597f67..d0707b0f067 100644 --- a/homeassistant/components/binary_sensor/knx.py +++ b/homeassistant/components/binary_sensor/knx.py @@ -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() diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py index c952e7c8987..c60463a8663 100644 --- a/homeassistant/components/binary_sensor/nest.py +++ b/homeassistant/components/binary_sensor/nest.py @@ -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) diff --git a/homeassistant/components/binary_sensor/tellduslive.py b/homeassistant/components/binary_sensor/tellduslive.py index c412ec37e51..450a5e580bd 100644 --- a/homeassistant/components/binary_sensor/tellduslive.py +++ b/homeassistant/components/binary_sensor/tellduslive.py @@ -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 diff --git a/homeassistant/components/camera/nest.py b/homeassistant/components/camera/nest.py index 175dbcd2267..e1d26371984 100644 --- a/homeassistant/components/camera/nest.py +++ b/homeassistant/components/camera/nest.py @@ -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) diff --git a/homeassistant/components/climate/knx.py b/homeassistant/components/climate/knx.py index ed197f57ab3..4eada356653 100644 --- a/homeassistant/components/climate/knx.py +++ b/homeassistant/components/climate/knx.py @@ -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) diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index 81c5fb3c2aa..321559f10ee 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -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 diff --git a/homeassistant/components/climate/radiotherm.py b/homeassistant/components/climate/radiotherm.py index 3d1d8e6a53e..429b544aefc 100644 --- a/homeassistant/components/climate/radiotherm.py +++ b/homeassistant/components/climate/radiotherm.py @@ -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. diff --git a/homeassistant/components/cover/knx.py b/homeassistant/components/cover/knx.py index 74ac80a476d..43a87fab367 100644 --- a/homeassistant/components/cover/knx.py +++ b/homeassistant/components/cover/knx.py @@ -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()) diff --git a/homeassistant/components/cover/tellduslive.py b/homeassistant/components/cover/tellduslive.py index fc352aa8482..9d292d9e8b5 100644 --- a/homeassistant/components/cover/tellduslive.py +++ b/homeassistant/components/cover/tellduslive.py @@ -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() diff --git a/homeassistant/components/juicenet.py b/homeassistant/components/juicenet.py index 2ed32521f1d..55567d45879 100644 --- a/homeassistant/components/juicenet.py +++ b/homeassistant/components/juicenet.py @@ -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) diff --git a/homeassistant/components/light/knx.py b/homeassistant/components/light/knx.py index 23929db8626..778d2fac59c 100644 --- a/homeassistant/components/light/knx.py +++ b/homeassistant/components/light/knx.py @@ -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() diff --git a/homeassistant/components/light/qwikswitch.py b/homeassistant/components/light/qwikswitch.py index fa986ff09f2..413358d9cee 100644 --- a/homeassistant/components/light/qwikswitch.py +++ b/homeassistant/components/light/qwikswitch.py @@ -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 diff --git a/homeassistant/components/light/tellduslive.py b/homeassistant/components/light/tellduslive.py index 6f39fb3b318..07b5458fa45 100644 --- a/homeassistant/components/light/tellduslive.py +++ b/homeassistant/components/light/tellduslive.py @@ -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() diff --git a/homeassistant/components/media_player/emby.py b/homeassistant/components/media_player/emby.py index b64aad38b3e..809db228d02 100644 --- a/homeassistant/components/media_player/emby.py +++ b/homeassistant/components/media_player/emby.py @@ -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) diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index 3c916860818..35906cf5023 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -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, diff --git a/homeassistant/components/media_player/soundtouch.py b/homeassistant/components/media_player/soundtouch.py index 489d028aad4..4e26af9dcc2 100644 --- a/homeassistant/components/media_player/soundtouch.py +++ b/homeassistant/components/media_player/soundtouch.py @@ -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]) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 04163f1ca13..57111350396 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -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('_', ' ')) diff --git a/homeassistant/components/notify/knx.py b/homeassistant/components/notify/knx.py index f9a6a4b25f2..750e3945569 100644 --- a/homeassistant/components/notify/knx.py +++ b/homeassistant/components/notify/knx.py @@ -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) diff --git a/homeassistant/components/qwikswitch.py b/homeassistant/components/qwikswitch.py index 8af0e8db28d..63e30a9491e 100644 --- a/homeassistant/components/qwikswitch.py +++ b/homeassistant/components/qwikswitch.py @@ -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.""" diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py index 7fbcba5a26e..723f575ba34 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -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", diff --git a/homeassistant/components/sensor/juicenet.py b/homeassistant/components/sensor/juicenet.py index b8ef38981e8..18725394a1f 100644 --- a/homeassistant/components/sensor/juicenet.py +++ b/homeassistant/components/sensor/juicenet.py @@ -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 diff --git a/homeassistant/components/sensor/knx.py b/homeassistant/components/sensor/knx.py index b8b55a1cc7c..ec506189c12 100644 --- a/homeassistant/components/sensor/knx.py +++ b/homeassistant/components/sensor/knx.py @@ -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): diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index d51b0ab4053..738bc53d880 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -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 diff --git a/homeassistant/components/sensor/tank_utility.py b/homeassistant/components/sensor/tank_utility.py index c3cc75dac0c..55928a80f13 100644 --- a/homeassistant/components/sensor/tank_utility.py +++ b/homeassistant/components/sensor/tank_utility.py @@ -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", {})) diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py index 34908595951..4676e08a247 100644 --- a/homeassistant/components/sensor/tellduslive.py +++ b/homeassistant/components/sensor/tellduslive.py @@ -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): diff --git a/homeassistant/components/switch/knx.py b/homeassistant/components/switch/knx.py index af60cee127a..678a8d4775f 100644 --- a/homeassistant/components/switch/knx.py +++ b/homeassistant/components/switch/knx.py @@ -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() diff --git a/homeassistant/components/switch/tellduslive.py b/homeassistant/components/switch/tellduslive.py index c1134fc21c1..0263dfd8198 100644 --- a/homeassistant/components/switch/tellduslive.py +++ b/homeassistant/components/switch/tellduslive.py @@ -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() diff --git a/homeassistant/components/tellduslive.py b/homeassistant/components/tellduslive.py index 58be267bbbc..693499510ad 100644 --- a/homeassistant/components/tellduslive.py +++ b/homeassistant/components/tellduslive.py @@ -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 From 3032de1dc1f4f8ca12aa7eab965fcc5873b21061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20GR=C3=89A?= Date: Sun, 26 Aug 2018 21:27:03 +0200 Subject: [PATCH 018/172] Inconsistent entity_id when multiple sensors (#16205) * Inconsistent entity_id when multiple sensors I am submitting a change to fix a [bug](https://github.com/home-assistant/home-assistant/issues/16204) for when there are several sensors for the same hostname. For example I want to track my IPv4 and IPv6 address. It creates two entities that regularly switch ids based on the order they get initialized. To fix this I comform to the way other componnents have addressed the issue by adding an optional `name` attribute. * Line too long * Removing trailing whitespace --- homeassistant/components/sensor/dnsip.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sensor/dnsip.py b/homeassistant/components/sensor/dnsip.py index ac681dc691a..3027b6f8ca6 100644 --- a/homeassistant/components/sensor/dnsip.py +++ b/homeassistant/components/sensor/dnsip.py @@ -19,11 +19,13 @@ REQUIREMENTS = ['aiodns==1.1.1'] _LOGGER = logging.getLogger(__name__) +CONF_NAME = 'name' CONF_HOSTNAME = 'hostname' CONF_RESOLVER = 'resolver' CONF_RESOLVER_IPV6 = 'resolver_ipv6' CONF_IPV6 = 'ipv6' +DEFAULT_NAME = 'myip' DEFAULT_HOSTNAME = 'myip.opendns.com' DEFAULT_RESOLVER = '208.67.222.222' DEFAULT_RESOLVER_IPV6 = '2620:0:ccc::2' @@ -32,6 +34,7 @@ DEFAULT_IPV6 = False SCAN_INTERVAL = timedelta(seconds=120) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_HOSTNAME, default=DEFAULT_HOSTNAME): cv.string, vol.Optional(CONF_RESOLVER, default=DEFAULT_RESOLVER): cv.string, vol.Optional(CONF_RESOLVER_IPV6, default=DEFAULT_RESOLVER_IPV6): cv.string, @@ -40,28 +43,34 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the DNS IP sensor.""" hostname = config.get(CONF_HOSTNAME) + name = config.get(CONF_NAME) + if not name: + if hostname == DEFAULT_HOSTNAME: + name = DEFAULT_NAME + else: + name = hostname ipv6 = config.get(CONF_IPV6) if ipv6: resolver = config.get(CONF_RESOLVER_IPV6) else: resolver = config.get(CONF_RESOLVER) - async_add_entities([WanIpSensor( - hass, hostname, resolver, ipv6)], True) + async_add_devices([WanIpSensor( + hass, name, hostname, resolver, ipv6)], True) class WanIpSensor(Entity): """Implementation of a DNS IP sensor.""" - def __init__(self, hass, hostname, resolver, ipv6): + def __init__(self, hass, name, hostname, resolver, ipv6): """Initialize the sensor.""" import aiodns self.hass = hass - self._name = hostname + self._name = name + self.hostname = hostname self.resolver = aiodns.DNSResolver(loop=self.hass.loop) self.resolver.nameservers = [resolver] self.querytype = 'AAAA' if ipv6 else 'A' @@ -80,7 +89,8 @@ class WanIpSensor(Entity): @asyncio.coroutine def async_update(self): """Get the current DNS IP address for hostname.""" - response = yield from self.resolver.query(self._name, self.querytype) + response = yield from self.resolver.query(self.hostname, + self.querytype) if response: self._state = response[0].host else: From d166f2da80d400f3bde6f4b4f3f656a711ab6728 Mon Sep 17 00:00:00 2001 From: Marcel Hoppe Date: Sun, 26 Aug 2018 21:28:42 +0200 Subject: [PATCH 019/172] remove hangouts.users state, simplifies hangouts.conversations (#16191) --- .../components/hangouts/hangouts_bot.py | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index d4c5606799d..d9ffb4cbace 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -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) From 499bb3f4a29e0f2bc647116e1180f02c424d6d7d Mon Sep 17 00:00:00 2001 From: PhracturedBlue Date: Sun, 26 Aug 2018 12:29:15 -0700 Subject: [PATCH 020/172] Handle exception from pillow (#16190) --- homeassistant/components/camera/proxy.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/camera/proxy.py b/homeassistant/components/camera/proxy.py index a19efcfb1af..6c245ffdf43 100644 --- a/homeassistant/components/camera/proxy.py +++ b/homeassistant/components/camera/proxy.py @@ -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) From b043ac0f7fdb0c8a4dead6755ac8daf85ce1dfa5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 26 Aug 2018 21:30:14 +0200 Subject: [PATCH 021/172] Update frontend to 20180826.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index c475ea55974..4622f80948e 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -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', diff --git a/requirements_all.txt b/requirements_all.txt index 41d716c28f1..2cd19d58ce6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -438,7 +438,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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index af68edbd632..69a02d5900d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 From 69d104bcb6faa05a022f84c4a1e867374bd03330 Mon Sep 17 00:00:00 2001 From: Penny Wood Date: Mon, 27 Aug 2018 03:35:06 +0800 Subject: [PATCH 022/172] Update aiohttp to version 3.4.0. (#16198) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 26628d7fe62..fdff380301f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,4 +1,4 @@ -aiohttp==3.3.2 +aiohttp==3.4.0 astral==1.6.1 async_timeout==3.0.0 attrs==18.1.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2cd19d58ce6..2f27662b2e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,5 +1,5 @@ # Home Assistant core -aiohttp==3.3.2 +aiohttp==3.4.0 astral==1.6.1 async_timeout==3.0.0 attrs==18.1.0 diff --git a/setup.py b/setup.py index 7484dc286e6..8e2ad008cc6 100755 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ 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', From 47755fb1e98cfda79ac4d354ae0b9a78073c97f4 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Sun, 26 Aug 2018 13:38:52 -0700 Subject: [PATCH 023/172] Add Time-based Onetime Password Multi-factor Authentication Module (#16129) * Add Time-based Onetime Password Multi-factor Auth Add TOTP setup flow, generate QR code * Resolve rebase issue * Use svg instead png for QR code * Lint and typing * Fix translation * Load totp auth module by default * use tag instead markdown image * Update strings * Cleanup --- homeassistant/auth/__init__.py | 8 +- homeassistant/auth/mfa_modules/totp.py | 212 ++++++++++++++++++ homeassistant/auth/providers/__init__.py | 4 +- .../components/auth/.translations/en.json | 16 ++ homeassistant/components/auth/strings.json | 16 ++ homeassistant/config.py | 6 +- requirements_all.txt | 4 + requirements_test_all.txt | 4 + script/gen_requirements_all.py | 1 + tests/auth/mfa_modules/test_totp.py | 130 +++++++++++ tests/test_config.py | 11 +- 11 files changed, 404 insertions(+), 8 deletions(-) create mode 100644 homeassistant/auth/mfa_modules/totp.py create mode 100644 homeassistant/components/auth/.translations/en.json create mode 100644 homeassistant/components/auth/strings.json create mode 100644 tests/auth/mfa_modules/test_totp.py diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index e0b7b377b1f..952bb3b8352 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -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) \ diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py new file mode 100644 index 00000000000..48531863c1a --- /dev/null +++ b/homeassistant/auth/mfa_modules/totp.py @@ -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('' + ' 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 + ) diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index e8ef7cbf3d4..0bcb47d4af9 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -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( diff --git a/homeassistant/components/auth/.translations/en.json b/homeassistant/components/auth/.translations/en.json new file mode 100644 index 00000000000..5c1af67b120 --- /dev/null +++ b/homeassistant/components/auth/.translations/en.json @@ -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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/strings.json b/homeassistant/components/auth/strings.json new file mode 100644 index 00000000000..b0083ab577b --- /dev/null +++ b/homeassistant/components/auth/strings.json @@ -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." + } + } + } +} diff --git a/homeassistant/config.py b/homeassistant/config.py index fe8f8ef0f60..a799094c94d 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -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 diff --git a/requirements_all.txt b/requirements_all.txt index 2f27662b2e7..691bbf62246 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -46,6 +46,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 @@ -985,6 +988,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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 69a02d5900d..5fa4af21a62 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index e26393bb800..fe23e638e5b 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -78,6 +78,7 @@ TEST_REQUIREMENTS = ( 'pylitejet', 'pymonoprice', 'pynx584', + 'pyotp', 'pyqwikswitch', 'PyRMVtransport', 'python-forecastio', diff --git a/tests/auth/mfa_modules/test_totp.py b/tests/auth/mfa_modules/test_totp.py new file mode 100644 index 00000000000..28e6c949bc4 --- /dev/null +++ b/tests/auth/mfa_modules/test_totp.py @@ -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' diff --git a/tests/test_config.py b/tests/test_config.py index 76ea576ac28..3cfe67f70b1 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -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): From bacecb4249209d4db4fd0beb45da06d2e0fd5591 Mon Sep 17 00:00:00 2001 From: Matt Hamilton Date: Sun, 26 Aug 2018 16:50:31 -0400 Subject: [PATCH 024/172] Replace pbkdf2 with bcrypt (#16071) * Replace pbkdf2 with bcrypt bcrypt isn't inherently better than pbkdf2, but everything "just works" out of the box. * the hash verification routine now only computes one hash per call * a per-user salt is built into the hash as opposed to the current global salt * bcrypt.checkpw() is immune to timing attacks regardless of input * hash strength is a function of real time benchmarks and a "difficulty" level, meaning we won't have to ever update the iteration count * WIP: add hash upgrade mechanism * WIP: clarify decode issue * remove stale testing code * Fix test * Ensure incorrect legacy passwords fail * Add better invalid legacy password test * Lint * Run tests in async scope --- homeassistant/auth/providers/homeassistant.py | 51 +++++++++-- homeassistant/package_constraints.txt | 1 + requirements_all.txt | 1 + setup.py | 1 + tests/auth/providers/test_homeassistant.py | 89 +++++++++++++++++++ 5 files changed, 135 insertions(+), 8 deletions(-) diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index ce252497901..c743a5b7f65 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -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): diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index fdff380301f..70fb519eef4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -2,6 +2,7 @@ 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 diff --git a/requirements_all.txt b/requirements_all.txt index 691bbf62246..e1b5b1c70ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3,6 +3,7 @@ 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 diff --git a/setup.py b/setup.py index 8e2ad008cc6..b1b0af70319 100755 --- a/setup.py +++ b/setup.py @@ -36,6 +36,7 @@ REQUIRES = [ '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', diff --git a/tests/auth/providers/test_homeassistant.py b/tests/auth/providers/test_homeassistant.py index c92f8539b17..935c5e50dd5 100644 --- a/tests/auth/providers/test_homeassistant.py +++ b/tests/auth/providers/test_homeassistant.py @@ -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 From 4da719f43cdc3ff747a58291e0e5a721a9bb772d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 26 Aug 2018 22:52:21 +0200 Subject: [PATCH 025/172] Update translations --- homeassistant/components/hangouts/.translations/en.json | 2 +- homeassistant/components/hangouts/.translations/pl.json | 2 +- homeassistant/components/hangouts/.translations/ru.json | 5 +++++ .../components/homematicip_cloud/.translations/pt-BR.json | 1 + .../components/homematicip_cloud/.translations/zh-Hant.json | 1 + homeassistant/components/hue/.translations/pt-BR.json | 2 +- 6 files changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hangouts/.translations/en.json b/homeassistant/components/hangouts/.translations/en.json index eb278afaf7f..6e70a1f4310 100644 --- a/homeassistant/components/hangouts/.translations/en.json +++ b/homeassistant/components/hangouts/.translations/en.json @@ -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": { diff --git a/homeassistant/components/hangouts/.translations/pl.json b/homeassistant/components/hangouts/.translations/pl.json index 9cbc02f126e..a8314761f8d 100644 --- a/homeassistant/components/hangouts/.translations/pl.json +++ b/homeassistant/components/hangouts/.translations/pl.json @@ -21,7 +21,7 @@ "email": "Adres e-mail", "password": "Has\u0142o" }, - "title": "Login Google Hangouts" + "title": "Logowanie do Google Hangouts" } }, "title": "Google Hangouts" diff --git a/homeassistant/components/hangouts/.translations/ru.json b/homeassistant/components/hangouts/.translations/ru.json index 730d9404837..c3363215201 100644 --- a/homeassistant/components/hangouts/.translations/ru.json +++ b/homeassistant/components/hangouts/.translations/ru.json @@ -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": { diff --git a/homeassistant/components/homematicip_cloud/.translations/pt-BR.json b/homeassistant/components/homematicip_cloud/.translations/pt-BR.json index 6e5af1c26cc..d4ecbe50107 100644 --- a/homeassistant/components/homematicip_cloud/.translations/pt-BR.json +++ b/homeassistant/components/homematicip_cloud/.translations/pt-BR.json @@ -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": { diff --git a/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json b/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json index d8c6cff9b0c..9340070d9a3 100644 --- a/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json +++ b/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json @@ -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": { diff --git a/homeassistant/components/hue/.translations/pt-BR.json b/homeassistant/components/hue/.translations/pt-BR.json index 5c6e409245c..b30764c9239 100644 --- a/homeassistant/components/hue/.translations/pt-BR.json +++ b/homeassistant/components/hue/.translations/pt-BR.json @@ -24,6 +24,6 @@ "title": "Hub de links" } }, - "title": "Philips Hue" + "title": "" } } \ No newline at end of file From 5d7a2f92df7a17900baadfde426e26ff9fb2aa1e Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Mon, 27 Aug 2018 06:06:46 +0200 Subject: [PATCH 026/172] Add temperature sensors to the velbus component (#16203) * Added support for velbus temperature sensors * Bumped the required version * updated requirements_all.txt * Auto review comments fixed * Updated after comments * Updated after comments * Fix travis * Fix travis --- homeassistant/components/sensor/velbus.py | 48 +++++++++++++++++++++++ homeassistant/components/velbus.py | 7 +++- requirements_all.txt | 2 +- 3 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/sensor/velbus.py diff --git a/homeassistant/components/sensor/velbus.py b/homeassistant/components/sensor/velbus.py new file mode 100644 index 00000000000..ea4af320add --- /dev/null +++ b/homeassistant/components/sensor/velbus.py @@ -0,0 +1,48 @@ +""" +Velbus sensors. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/sensor.velbus/ +""" +import logging + +from homeassistant.const import ( + TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE) +from homeassistant.components.velbus import ( + DOMAIN as VELBUS_DOMAIN, VelbusEntity) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['velbus'] + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the Velbus temp sensor platform.""" + if discovery_info is None: + return + sensors = [] + for sensor in discovery_info: + module = hass.data[VELBUS_DOMAIN].get_module(sensor[0]) + channel = sensor[1] + sensors.append(VelbusTempSensor(module, channel)) + async_add_entities(sensors) + + +class VelbusTempSensor(VelbusEntity): + """Representation of a temperature sensor.""" + + @property + def device_class(self): + """Return the device class of the sensor.""" + return DEVICE_CLASS_TEMPERATURE + + @property + def state(self): + """Return the state of the sensor.""" + return self._module.getCurTemp() + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return TEMP_CELSIUS diff --git a/homeassistant/components/velbus.py b/homeassistant/components/velbus.py index a6cdcc7cf90..d2def6f96bc 100644 --- a/homeassistant/components/velbus.py +++ b/homeassistant/components/velbus.py @@ -12,7 +12,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_PORT from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['python-velbus==2.0.18'] +REQUIREMENTS = ['python-velbus==2.0.19'] _LOGGER = logging.getLogger(__name__) @@ -47,7 +47,8 @@ async def async_setup(hass, config): modules = controller.get_modules() discovery_info = { 'switch': [], - 'binary_sensor': [] + 'binary_sensor': [], + 'temp_sensor': [] } for module in modules: for channel in range(1, module.number_of_channels() + 1): @@ -61,6 +62,8 @@ async def async_setup(hass, config): discovery_info['switch'], config) load_platform(hass, 'binary_sensor', DOMAIN, discovery_info['binary_sensor'], config) + load_platform(hass, 'sensor', DOMAIN, + discovery_info['temp_sensor'], config) controller.scan(callback) diff --git a/requirements_all.txt b/requirements_all.txt index e1b5b1c70ec..959fd21732f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1139,7 +1139,7 @@ python-telegram-bot==10.1.0 python-twitch==1.3.0 # homeassistant.components.velbus -python-velbus==2.0.18 +python-velbus==2.0.19 # homeassistant.components.media_player.vlc python-vlc==1.1.2 From a439690bd793392fa79151601425ce322fb4497d Mon Sep 17 00:00:00 2001 From: Jonas Karlsson <1937941+endor-force@users.noreply.github.com> Date: Mon, 27 Aug 2018 06:19:51 +0200 Subject: [PATCH 027/172] Rewrite of Trafikverket weather - Multiple sensor types supported (#15935) * Added precipitation type from API Enables users to see type of precipitation. Value returned from API is a string in swedish. * Corrected tox verification errors Correction of tox findings * Missed in tox - fixed * Hound witespace fix * Updated comment to trigger travis rebuild Travis tox failed due to problem with tox build process. Correcting in a comment to trigger retry in travis.. * Try to retrigger travis/tox successful rebuild * Cleaning * Cleaning more * Trafikverket rebuilt for library Extended pytrafikverket with weather sensor collction Changed behaviour of sensor component to use pytrafikverket. Added more sensors. User need to change config to use new version. [] Documentation needs to be updated * Cleaned up based on Martins input Appreciate the feedback --- .../sensor/trafikverket_weatherstation.py | 126 +++++++++--------- requirements_all.txt | 3 + 2 files changed, 64 insertions(+), 65 deletions(-) diff --git a/homeassistant/components/sensor/trafikverket_weatherstation.py b/homeassistant/components/sensor/trafikverket_weatherstation.py index a8ce6917dd3..433bb8e9ed1 100644 --- a/homeassistant/components/sensor/trafikverket_weatherstation.py +++ b/homeassistant/components/sensor/trafikverket_weatherstation.py @@ -4,119 +4,115 @@ Weather information for air and road temperature, provided by Trafikverket. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.trafikverket_weatherstation/ """ + +import asyncio from datetime import timedelta -import json import logging -import requests +import aiohttp import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME, CONF_TYPE, TEMP_CELSIUS) + ATTR_ATTRIBUTION, CONF_API_KEY, CONF_MONITORED_CONDITIONS, CONF_NAME) +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle +REQUIREMENTS = ['pytrafikverket==0.1.5.8'] + _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Data provided by Trafikverket API" +SCAN_INTERVAL = timedelta(seconds=300) +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) +CONF_ATTRIBUTION = "Data provided by Trafikverket API" CONF_STATION = 'station' -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) -SCAN_INTERVAL = timedelta(seconds=300) +SENSOR_TYPES = { + 'air_temp': ['Air temperature', '°C', 'air_temp'], + 'road_temp': ['Road temperature', '°C', 'road_temp'], + 'precipitation': ['Precipitation type', None, 'precipitationtype'], + 'wind_direction': ['Wind direction', '°', 'winddirection'], + 'wind_direction_text': ['Wind direction text', None, 'winddirectiontext'], + 'wind_speed': ['Wind speed', 'm/s', 'windforce'], + 'humidity': ['Humidity', '%', 'humidity'], +} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_NAME): cv.string, vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_STATION): cv.string, - vol.Required(CONF_TYPE): vol.In(['air', 'road']), + vol.Required(CONF_MONITORED_CONDITIONS, default=[]): + [vol.In(SENSOR_TYPES)], }) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Trafikverket sensor platform.""" - sensor_name = config.get(CONF_NAME) - sensor_api = config.get(CONF_API_KEY) - sensor_station = config.get(CONF_STATION) - sensor_type = config.get(CONF_TYPE) + from pytrafikverket.trafikverket_weather import TrafikverketWeather - add_entities([TrafikverketWeatherStation( - sensor_name, sensor_api, sensor_station, sensor_type)], True) + sensor_name = config[CONF_NAME] + sensor_api = config[CONF_API_KEY] + sensor_station = config[CONF_STATION] + + web_session = async_get_clientsession(hass) + + weather_api = TrafikverketWeather(web_session, sensor_api) + + dev = [] + for condition in config[CONF_MONITORED_CONDITIONS]: + dev.append(TrafikverketWeatherStation( + weather_api, sensor_name, condition, sensor_station)) + + if dev: + async_add_entities(dev, True) class TrafikverketWeatherStation(Entity): """Representation of a Trafikverket sensor.""" - def __init__(self, sensor_name, sensor_api, sensor_station, sensor_type): - """Initialize the Trafikverket sensor.""" - self._name = sensor_name - self._api = sensor_api - self._station = sensor_station + def __init__(self, weather_api, name, sensor_type, sensor_station): + """Initialize the sensor.""" + self._client = name + self._name = SENSOR_TYPES[sensor_type][0] self._type = sensor_type self._state = None + self._unit = SENSOR_TYPES[sensor_type][1] + self._station = sensor_station + self._weather_api = weather_api self._attributes = { ATTR_ATTRIBUTION: CONF_ATTRIBUTION, } + self._weather = None @property def name(self): """Return the name of the sensor.""" - return self._name + return '{} {}'.format(self._client, self._name) @property def state(self): - """Return the state of the sensor.""" + """Return the state of the device.""" return self._state @property def unit_of_measurement(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._attributes + """Return the unit of measurement of this entity, if any.""" + return self._unit @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Fetch new state data for the sensor.""" - url = 'http://api.trafikinfo.trafikverket.se/v1.3/data.json' - - if self._type == 'road': - air_vs_road = 'Road' - else: - air_vs_road = 'Air' - - xml = """ - - - - - - - Measurement.""" + air_vs_road + """.Temp - - """ - - # Testing JSON post request. + async def async_update(self): + """Get the latest data from Trafikverket and updates the states.""" try: - post = requests.post(url, data=xml.encode('utf-8'), timeout=5) - except requests.exceptions.RequestException as err: - _LOGGER.error("Please check network connection: %s", err) - return - - # Checking JSON respons. - try: - data = json.loads(post.text) - result = data["RESPONSE"]["RESULT"][0] - final = result["WeatherStation"][0]["Measurement"] - except KeyError: - _LOGGER.error("Incorrect weather station or API key") - return - - # air_vs_road contains "Air" or "Road" depending on user input. - self._state = final[air_vs_road]["Temp"] + self._weather = await self._weather_api.async_get_weather( + self._station) + self._state = getattr( + self._weather, + SENSOR_TYPES[self._type][2]) + except (asyncio.TimeoutError, + aiohttp.ClientError, ValueError) as error: + _LOGGER.error("Couldn't fetch weather data: %s", error) diff --git a/requirements_all.txt b/requirements_all.txt index 959fd21732f..92aeec6aefe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1171,6 +1171,9 @@ pytrackr==0.0.5 # homeassistant.components.tradfri pytradfri[async]==5.5.1 +# homeassistant.components.sensor.trafikverket_weatherstation +pytrafikverket==0.1.5.8 + # homeassistant.components.device_tracker.unifi pyunifi==2.13 From dec2d8d5b0d93d3a0ded66ed4847f86e839850fc Mon Sep 17 00:00:00 2001 From: Hunter Horsman Date: Mon, 27 Aug 2018 03:08:23 -0400 Subject: [PATCH 028/172] Add device_tracker.bluetooth_update service (#15252) * Add device_tracker.bluetooth_update service Will immediately scan for Bluetooth devices outside of the interval timer. Allows for less frequent scanning, with scanning on demand via automation. * remove excess whitespace per bot comments * Refactored update_bluetooth to call new function update_bluetooth_once * Change service name to bluetooth_tracker_update to reflect platform name * Reformat for line length * Linting fix, pydoc, first line should end with a period * Fixed a method call, and removed some more unsused parameters --- .../device_tracker/bluetooth_tracker.py | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/device_tracker/bluetooth_tracker.py b/homeassistant/components/device_tracker/bluetooth_tracker.py index 2ca519d225c..217df0aacd4 100644 --- a/homeassistant/components/device_tracker/bluetooth_tracker.py +++ b/homeassistant/components/device_tracker/bluetooth_tracker.py @@ -12,7 +12,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.components.device_tracker import ( YAML_DEVICES, CONF_TRACK_NEW, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL, - load_config, PLATFORM_SCHEMA, DEFAULT_TRACK_NEW, SOURCE_TYPE_BLUETOOTH) + load_config, PLATFORM_SCHEMA, DEFAULT_TRACK_NEW, SOURCE_TYPE_BLUETOOTH, + DOMAIN) import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -79,7 +80,13 @@ def setup_scanner(hass, config, see, discovery_info=None): request_rssi = config.get(CONF_REQUEST_RSSI, False) - def update_bluetooth(now): + def update_bluetooth(): + """Update Bluetooth and set timer for the next update.""" + update_bluetooth_once() + track_point_in_utc_time( + hass, update_bluetooth, dt_util.utcnow() + interval) + + def update_bluetooth_once(): """Lookup Bluetooth device and update status.""" try: if track_new: @@ -99,9 +106,14 @@ def setup_scanner(hass, config, see, discovery_info=None): see_device(mac, result, rssi) except bluetooth.BluetoothError: _LOGGER.exception("Error looking up Bluetooth device") - track_point_in_utc_time( - hass, update_bluetooth, dt_util.utcnow() + interval) - update_bluetooth(dt_util.utcnow()) + def handle_update_bluetooth(call): + """Update bluetooth devices on demand.""" + update_bluetooth_once() + + update_bluetooth() + + hass.services.register( + DOMAIN, "bluetooth_tracker_update", handle_update_bluetooth) return True From 2e9db1f5c4df8d8acd05daa112fc1b27110de28a Mon Sep 17 00:00:00 2001 From: Julian Kahnert Date: Mon, 27 Aug 2018 09:39:11 +0200 Subject: [PATCH 029/172] Fix geizhals price parsing (#15990) * fix geizhals price parsing * Fix lint issue * switch to the geizhals pypi package * throttle updates * update geizhals version * initialize empty device * minor changes to trigger another TravisCI test * device => _device * bump geizhals version --- homeassistant/components/sensor/geizhals.py | 109 ++++++-------------- requirements_all.txt | 4 +- 2 files changed, 35 insertions(+), 78 deletions(-) diff --git a/homeassistant/components/sensor/geizhals.py b/homeassistant/components/sensor/geizhals.py index 2c7325866ac..7d215fb6baf 100644 --- a/homeassistant/components/sensor/geizhals.py +++ b/homeassistant/components/sensor/geizhals.py @@ -13,15 +13,15 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle from homeassistant.helpers.entity import Entity -from homeassistant.const import (CONF_DOMAIN, CONF_NAME) +from homeassistant.const import CONF_NAME -REQUIREMENTS = ['beautifulsoup4==4.6.3'] +REQUIREMENTS = ['geizhals==0.0.7'] _LOGGER = logging.getLogger(__name__) CONF_DESCRIPTION = 'description' CONF_PRODUCT_ID = 'product_id' -CONF_REGEX = 'regex' +CONF_LOCALE = 'locale' ICON = 'mdi:coin' @@ -31,13 +31,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_NAME): cv.string, vol.Required(CONF_PRODUCT_ID): cv.positive_int, vol.Optional(CONF_DESCRIPTION, default='Price'): cv.string, - vol.Optional(CONF_DOMAIN, default='geizhals.de'): vol.In( - ['geizhals.at', - 'geizhals.eu', - 'geizhals.de', - 'skinflint.co.uk', - 'cenowarka.pl']), - vol.Optional(CONF_REGEX, default=r'\D\s(\d*)[\,|\.](\d*)'): cv.string, + vol.Optional(CONF_LOCALE, default='DE'): vol.In( + ['AT', + 'EU', + 'DE', + 'UK', + 'PL']), }) @@ -46,22 +45,27 @@ def setup_platform(hass, config, add_entities, discovery_info=None): name = config.get(CONF_NAME) description = config.get(CONF_DESCRIPTION) product_id = config.get(CONF_PRODUCT_ID) - domain = config.get(CONF_DOMAIN) - regex = config.get(CONF_REGEX) + domain = config.get(CONF_LOCALE) - add_entities([Geizwatch(name, description, product_id, domain, regex)], + add_entities([Geizwatch(name, description, product_id, domain)], True) class Geizwatch(Entity): """Implementation of Geizwatch.""" - def __init__(self, name, description, product_id, domain, regex): + def __init__(self, name, description, product_id, domain): """Initialize the sensor.""" + from geizhals import Device, Geizhals + + # internal self._name = name + self._geizhals = Geizhals(product_id, domain) + self._device = Device() + + # external self.description = description - self.data = GeizParser(product_id, domain, regex) - self._state = None + self.product_id = product_id @property def name(self): @@ -76,73 +80,24 @@ class Geizwatch(Entity): @property def state(self): """Return the best price of the selected product.""" - return self._state + return self._device.prices[0] @property def device_state_attributes(self): """Return the state attributes.""" - while len(self.data.prices) < 4: - self.data.prices.append("None") - attrs = {'device_name': self.data.device_name, + while len(self._device.prices) < 4: + self._device.prices.append('None') + attrs = {'device_name': self._device.name, 'description': self.description, - 'unit_of_measurement': self.data.unit_of_measurement, - 'product_id': self.data.product_id, - 'price1': self.data.prices[0], - 'price2': self.data.prices[1], - 'price3': self.data.prices[2], - 'price4': self.data.prices[3]} + 'unit_of_measurement': self._device.price_currency, + 'product_id': self.product_id, + 'price1': self._device.prices[0], + 'price2': self._device.prices[1], + 'price3': self._device.prices[2], + 'price4': self._device.prices[3]} return attrs - def update(self): - """Get the latest price from geizhals and updates the state.""" - self.data.update() - self._state = self.data.prices[0] - - -class GeizParser: - """Pull data from the geizhals website.""" - - def __init__(self, product_id, domain, regex): - """Initialize the sensor.""" - # parse input arguments - self.product_id = product_id - self.domain = domain - self.regex = regex - - # set some empty default values - self.device_name = '' - self.prices = [None, None, None, None] - self.unit_of_measurement = '' - @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): - """Update the device prices.""" - import bs4 - import requests - import re - - sess = requests.session() - request = sess.get('https://{}/{}'.format(self.domain, - self.product_id), - allow_redirects=True, - timeout=1) - soup = bs4.BeautifulSoup(request.text, 'html.parser') - - # parse name - raw = soup.find_all('span', attrs={'itemprop': 'name'}) - self.device_name = raw[1].string - - # parse prices - prices = [] - for tmp in soup.find_all('span', attrs={'class': 'gh_price'}): - matches = re.search(self.regex, tmp.string) - raw = '{}.{}'.format(matches.group(1), - matches.group(2)) - prices += [float(raw)] - prices.sort() - self.prices = prices[1:] - - # parse unit - price_match = soup.find('span', attrs={'class': 'gh_price'}) - matches = re.search(r'€|£|PLN', price_match.string) - self.unit_of_measurement = matches.group() + """Get the latest price from geizhals and updates the state.""" + self._device = self._geizhals.parse() diff --git a/requirements_all.txt b/requirements_all.txt index 92aeec6aefe..2a238a933b8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -163,7 +163,6 @@ batinfo==0.4.2 # beacontools[scan]==1.2.3 # homeassistant.components.device_tracker.linksys_ap -# homeassistant.components.sensor.geizhals # homeassistant.components.sensor.scrape # homeassistant.components.sensor.sytadin beautifulsoup4==4.6.3 @@ -387,6 +386,9 @@ gTTS-token==1.1.1 # homeassistant.components.sensor.gearbest gearbest_parser==1.0.7 +# homeassistant.components.sensor.geizhals +geizhals==0.0.7 + # homeassistant.components.sensor.gitter gitterpy==0.1.7 From f1e378bff8eeeced2c86ef7a046ce8e526787df5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 26 Aug 2018 22:52:58 +0200 Subject: [PATCH 030/172] Add new translations --- .../components/hangouts/.translations/it.json | 5 +++ .../components/hangouts/.translations/no.json | 13 ++++++++ .../hangouts/.translations/pt-BR.json | 28 +++++++++++++++++ .../hangouts/.translations/zh-Hant.json | 31 +++++++++++++++++++ .../homematicip_cloud/.translations/it.json | 11 +++++++ .../sensor/.translations/moon.it.json | 8 +++++ .../sensor/.translations/moon.pt-BR.json | 12 +++++++ 7 files changed, 108 insertions(+) create mode 100644 homeassistant/components/hangouts/.translations/it.json create mode 100644 homeassistant/components/hangouts/.translations/no.json create mode 100644 homeassistant/components/hangouts/.translations/pt-BR.json create mode 100644 homeassistant/components/hangouts/.translations/zh-Hant.json create mode 100644 homeassistant/components/homematicip_cloud/.translations/it.json create mode 100644 homeassistant/components/sensor/.translations/moon.it.json create mode 100644 homeassistant/components/sensor/.translations/moon.pt-BR.json diff --git a/homeassistant/components/hangouts/.translations/it.json b/homeassistant/components/hangouts/.translations/it.json new file mode 100644 index 00000000000..0c609b3430a --- /dev/null +++ b/homeassistant/components/hangouts/.translations/it.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Google Hangouts" + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/no.json b/homeassistant/components/hangouts/.translations/no.json new file mode 100644 index 00000000000..7ea074470c7 --- /dev/null +++ b/homeassistant/components/hangouts/.translations/no.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "email": "E-postadresse", + "password": "Passord" + } + } + }, + "title": "Google Hangouts" + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/pt-BR.json b/homeassistant/components/hangouts/.translations/pt-BR.json new file mode 100644 index 00000000000..4dffe492c4d --- /dev/null +++ b/homeassistant/components/hangouts/.translations/pt-BR.json @@ -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" + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/zh-Hant.json b/homeassistant/components/hangouts/.translations/zh-Hant.json new file mode 100644 index 00000000000..0920e0325d2 --- /dev/null +++ b/homeassistant/components/hangouts/.translations/zh-Hant.json @@ -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" + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/it.json b/homeassistant/components/homematicip_cloud/.translations/it.json new file mode 100644 index 00000000000..2566eb25570 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/.translations/it.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "init": { + "data": { + "pin": "Codice Pin (opzionale)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.it.json b/homeassistant/components/sensor/.translations/moon.it.json new file mode 100644 index 00000000000..fce5152b3f9 --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.it.json @@ -0,0 +1,8 @@ +{ + "state": { + "first_quarter": "Primo quarto", + "full_moon": "Luna piena", + "last_quarter": "Ultimo quarto", + "new_moon": "Nuova luna" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.pt-BR.json b/homeassistant/components/sensor/.translations/moon.pt-BR.json new file mode 100644 index 00000000000..af4cefff6e5 --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.pt-BR.json @@ -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" + } +} \ No newline at end of file From 94662620e214ef30d01d7e6661fa2fd874154b86 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 27 Aug 2018 10:16:59 +0200 Subject: [PATCH 031/172] Update translations --- .../components/auth/.translations/ca.json | 16 ++++++++++++++++ .../components/auth/.translations/en.json | 6 +++--- 2 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/auth/.translations/ca.json diff --git a/homeassistant/components/auth/.translations/ca.json b/homeassistant/components/auth/.translations/ca.json new file mode 100644 index 00000000000..1b3b25dbcff --- /dev/null +++ b/homeassistant/components/auth/.translations/ca.json @@ -0,0 +1,16 @@ +{ + "mfa_setup": { + "totp": { + "error": { + "invalid_code": "Codi no v\u00e0lid, si us plau torni a provar-ho. Si obteniu aquest error repetidament, assegureu-vos que la data i hora de Home Assistant sigui correcta i precisa." + }, + "step": { + "init": { + "description": "Per activar la verificaci\u00f3 en dos passos mitjan\u00e7ant contrasenyes d'un sol \u00fas basades en temps, escanegeu el codi QR amb la vostre aplicaci\u00f3 de verificaci\u00f3. Si no en teniu cap, us recomanem [Google Authenticator](https://support.google.com/accounts/answer/1066447) o b\u00e9 [Authy](https://authy.com/). \n\n {qr_code} \n \nDespr\u00e9s d'escanejar el codi QR, introdu\u00efu el codi de sis d\u00edgits proporcionat per l'aplicaci\u00f3. Si teniu problemes per escanejar el codi QR, feu una configuraci\u00f3 manual amb el codi **`{code}`**.", + "title": "Configureu la verificaci\u00f3 en dos passos utilitzant TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/en.json b/homeassistant/components/auth/.translations/en.json index 5c1af67b120..a0fd20e9d08 100644 --- a/homeassistant/components/auth/.translations/en.json +++ b/homeassistant/components/auth/.translations/en.json @@ -2,12 +2,12 @@ "mfa_setup": { "totp": { "error": { - "invalid_code": "Invalid code, please try again. If you get this error consistently, please make sure the clock on Home Assistant system is accurate." + "invalid_code": "Invalid code, please try again. If you get this error consistently, please make sure the clock of your Home Assistant system is accurate." }, "step": { "init": { - "description": "Scan the QR code with your authentication app, such as **Google Authenticator** or **Authy**. If you have problem to scan the QR code, using **`{code}`** to manual setup. \n\n{qr_code}\n\nEnter the six digi code appeared in your app below to verify the setup:", - "title": "Scan this QR code with your app" + "description": "To activate two factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator](https://support.google.com/accounts/answer/1066447) or [Authy](https://authy.com/).\n\n{qr_code}\n\nAfter scanning the code, enter the six digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**.", + "title": "Set up two-factor authentication using TOTP" } }, "title": "TOTP" From 9d491f532297a49399201beccd602d6643ac5e29 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 27 Aug 2018 10:37:03 +0200 Subject: [PATCH 032/172] Change auth warning (#16216) --- homeassistant/components/http/__init__.py | 16 ++++------------ homeassistant/components/http/auth.py | 2 +- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index ac08c26229c..6909a0e4664 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -200,18 +200,10 @@ class HomeAssistantHTTP: if is_ban_enabled: setup_bans(hass, app, login_threshold) - if hass.auth.active: - if hass.auth.support_legacy: - _LOGGER.warning("Experimental auth api enabled and " - "legacy_api_password support enabled. Please " - "use access_token instead api_password, " - "although you can still use legacy " - "api_password") - else: - _LOGGER.warning("Experimental auth api enabled. Please use " - "access_token instead api_password.") - elif api_password is None: - _LOGGER.warning("You have been advised to set http.api_password.") + if hass.auth.active and hass.auth.support_legacy: + _LOGGER.warning( + "legacy_api_password support has been enabled. If you don't" + "require it, remove the 'api_password' from your http config.") setup_auth(app, trusted_networks, hass.auth.active, support_legacy=hass.auth.support_legacy, diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 7adcc43f4af..a18b4de7a10 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -32,7 +32,7 @@ def setup_auth(app, trusted_networks, use_auth, if request.path not in old_auth_warning: _LOGGER.log( logging.INFO if support_legacy else logging.WARNING, - 'Please change to use bearer token access %s from %s', + 'You need to use a bearer token to access %s from %s', request.path, request[KEY_REAL_IP]) old_auth_warning.add(request.path) From c51170ef6de4c58c065b2db060bafc5cecba053e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 27 Aug 2018 15:05:36 +0200 Subject: [PATCH 033/172] Add Volkszaehler sensor (#16188) * Add Volkszaehler sensor * Update icons * Improve code --- .coveragerc | 1 + .../components/sensor/volkszaehler.py | 138 ++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 142 insertions(+) create mode 100644 homeassistant/components/sensor/volkszaehler.py diff --git a/.coveragerc b/.coveragerc index bb0be2d9433..449883265f6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -759,6 +759,7 @@ omit = homeassistant/components/sensor/uscis.py homeassistant/components/sensor/vasttrafik.py homeassistant/components/sensor/viaggiatreno.py + homeassistant/components/sensor/volkszaehler.py homeassistant/components/sensor/waqi.py homeassistant/components/sensor/waze_travel_time.py homeassistant/components/sensor/whois.py diff --git a/homeassistant/components/sensor/volkszaehler.py b/homeassistant/components/sensor/volkszaehler.py new file mode 100644 index 00000000000..47aa580e3d4 --- /dev/null +++ b/homeassistant/components/sensor/volkszaehler.py @@ -0,0 +1,138 @@ +""" +Support for consuming values for the Volkszaehler API. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.volkszaehler/ +""" +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_HOST, CONF_NAME, CONF_PORT, CONF_MONITORED_CONDITIONS) +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +REQUIREMENTS = ['volkszaehler==0.1.2'] + +_LOGGER = logging.getLogger(__name__) + +CONF_UUID = 'uuid' + +DEFAULT_HOST = 'localhost' +DEFAULT_NAME = 'Volkszaehler' +DEFAULT_PORT = 80 + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) + +SENSOR_TYPES = { + 'average': ['Average', 'W', 'mdi:power-off'], + 'consumption': ['Consumption', 'Wh', 'mdi:power-plug'], + 'max': ['Max', 'W', 'mdi:arrow-up'], + 'min': ['Min', 'W', 'mdi:arrow-down'], +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_UUID): cv.string, + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_MONITORED_CONDITIONS, default=['average']): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), +}) + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up the Volkszaehler sensors.""" + from volkszaehler import Volkszaehler + + host = config[CONF_HOST] + name = config[CONF_NAME] + port = config[CONF_PORT] + uuid = config[CONF_UUID] + conditions = config[CONF_MONITORED_CONDITIONS] + + session = async_get_clientsession(hass) + vz_api = VolkszaehlerData( + Volkszaehler(hass.loop, session, uuid, host=host, port=port)) + + await vz_api.async_update() + + if vz_api.api.data is None: + raise PlatformNotReady + + dev = [] + for condition in conditions: + dev.append(VolkszaehlerSensor(vz_api, name, condition)) + + async_add_entities(dev, True) + + +class VolkszaehlerSensor(Entity): + """Implementation of a Volkszaehler sensor.""" + + def __init__(self, vz_api, name, sensor_type): + """Initialize the Volkszaehler sensor.""" + self.vz_api = vz_api + self._name = name + self.type = sensor_type + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return '{} {}'.format(self._name, SENSOR_TYPES[self.type][0]) + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return SENSOR_TYPES[self.type][2] + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return SENSOR_TYPES[self.type][1] + + @property + def available(self): + """Could the device be accessed during the last update call.""" + return self.vz_api.available + + @property + def state(self): + """Return the state of the resources.""" + return self._state + + async def async_update(self): + """Get the latest data from REST API.""" + await self.vz_api.async_update() + + if self.vz_api.api.data is not None: + self._state = round(getattr(self.vz_api.api, self.type), 2) + + +class VolkszaehlerData: + """The class for handling the data retrieval from the Volkszaehler API.""" + + def __init__(self, api): + """Initialize the data object.""" + self.api = api + self.available = True + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def async_update(self): + """Get the latest data from the Volkszaehler REST API.""" + from volkszaehler.exceptions import VolkszaehlerApiConnectionError + + try: + await self.api.get_data() + self.available = True + except VolkszaehlerApiConnectionError: + _LOGGER.error("Unable to fetch data from the Volkszaehler API") + self.available = False diff --git a/requirements_all.txt b/requirements_all.txt index 2a238a933b8..81de7219b60 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1431,6 +1431,9 @@ uvcclient==0.10.1 # homeassistant.components.climate.venstar venstarcolortouch==0.6 +# homeassistant.components.sensor.volkszaehler +volkszaehler==0.1.2 + # homeassistant.components.config.config_entries voluptuous-serialize==2.0.0 From 8435d2f53d787c2b3e07229a6273bb68d6e81d13 Mon Sep 17 00:00:00 2001 From: Daniel Bowman Date: Mon, 27 Aug 2018 16:17:43 +0100 Subject: [PATCH 034/172] openalpr flag `WITH_TEST` should be `WITH_TESTS` (#16218) Removes warning from openalpr build and saves a few seconds from build time as tests weren't being bypassed as intended --- virtualization/Docker/scripts/openalpr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/virtualization/Docker/scripts/openalpr b/virtualization/Docker/scripts/openalpr index b9e403710f1..38669f8175b 100755 --- a/virtualization/Docker/scripts/openalpr +++ b/virtualization/Docker/scripts/openalpr @@ -23,7 +23,7 @@ mkdir -p build cd build # Setup the compile environment -cmake -DWITH_TEST=FALSE -DWITH_BINDING_JAVA=FALSE --DWITH_BINDING_PYTHON=FALSE --DWITH_BINDING_GO=FALSE -DWITH_DAEMON=FALSE -DCMAKE_INSTALL_PREFIX:PATH=/usr/local .. +cmake -DWITH_TESTS=FALSE -DWITH_BINDING_JAVA=FALSE --DWITH_BINDING_PYTHON=FALSE --DWITH_BINDING_GO=FALSE -DWITH_DAEMON=FALSE -DCMAKE_INSTALL_PREFIX:PATH=/usr/local .. # compile the library make -j$(nproc) From 24aa580b63c676b1dd4809d1057aa9f1f287e9a0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 27 Aug 2018 21:56:28 +0200 Subject: [PATCH 035/172] Fix device telldus (#16224) --- homeassistant/components/tellduslive.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/tellduslive.py b/homeassistant/components/tellduslive.py index 693499510ad..c2b7ba9ba0f 100644 --- a/homeassistant/components/tellduslive.py +++ b/homeassistant/components/tellduslive.py @@ -287,7 +287,6 @@ class TelldusLiveEntity(Entity): self._id = device_id self._client = hass.data[DOMAIN] self._client.entities.append(self) - self.device = self._client.device(device_id) self._name = self.device.name _LOGGER.debug('Created device %s', self) From 943260fcd6644fcfec4b4af03402adfc5edb38e4 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 27 Aug 2018 22:00:20 +0200 Subject: [PATCH 036/172] Upgrade alpha_vantage to 2.1.0 (#16217) --- homeassistant/components/sensor/alpha_vantage.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/alpha_vantage.py b/homeassistant/components/sensor/alpha_vantage.py index c0b280d2d69..79943a8b084 100644 --- a/homeassistant/components/sensor/alpha_vantage.py +++ b/homeassistant/components/sensor/alpha_vantage.py @@ -15,7 +15,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['alpha_vantage==2.0.0'] +REQUIREMENTS = ['alpha_vantage==2.1.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 81de7219b60..42fdb321857 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -121,7 +121,7 @@ aladdin_connect==0.3 alarmdecoder==1.13.2 # homeassistant.components.sensor.alpha_vantage -alpha_vantage==2.0.0 +alpha_vantage==2.1.0 # homeassistant.components.amcrest amcrest==1.2.3 From 6f0c30ff8419c949ac1ebd79b4cfafaadb4bd1b2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 27 Aug 2018 22:28:17 +0200 Subject: [PATCH 037/172] Bump frontend to 20180827.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 4622f80948e..f0976c78224 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180826.0'] +REQUIREMENTS = ['home-assistant-frontend==20180827.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 42fdb321857..85af517bb29 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -444,7 +444,7 @@ hole==0.3.0 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180826.0 +home-assistant-frontend==20180827.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5fa4af21a62..5dcf0550aba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -84,7 +84,7 @@ hbmqtt==0.9.2 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180826.0 +home-assistant-frontend==20180827.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 45649824ca65b49cc676393d660e8c18dd488acf Mon Sep 17 00:00:00 2001 From: Marcel Hoppe Date: Tue, 28 Aug 2018 00:20:12 +0200 Subject: [PATCH 038/172] rewrite hangouts to use intents instead of commands (#16220) * rewrite hangouts to use intents instead of commands * small fixes * remove configured_hangouts check and CONFIG_SCHEMA * Lint * add import from .config_flow --- .../components/conversation/__init__.py | 38 +--- homeassistant/components/conversation/util.py | 35 ++++ homeassistant/components/hangouts/__init__.py | 53 +++++- homeassistant/components/hangouts/const.py | 26 +-- .../components/hangouts/hangouts_bot.py | 174 ++++++++++-------- tests/components/test_conversation.py | 12 +- 6 files changed, 196 insertions(+), 142 deletions(-) create mode 100644 homeassistant/components/conversation/util.py diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index 9cb00a84583..d8d386f5ca0 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant import core from homeassistant.components import http +from homeassistant.components.conversation.util import create_matcher from homeassistant.components.http.data_validator import ( RequestDataValidator) from homeassistant.components.cover import (INTENT_OPEN_COVER, @@ -74,7 +75,7 @@ def async_register(hass, intent_type, utterances): if isinstance(utterance, REGEX_TYPE): conf.append(utterance) else: - conf.append(_create_matcher(utterance)) + conf.append(create_matcher(utterance)) async def async_setup(hass, config): @@ -91,7 +92,7 @@ async def async_setup(hass, config): if conf is None: conf = intents[intent_type] = [] - conf.extend(_create_matcher(utterance) for utterance in utterances) + conf.extend(create_matcher(utterance) for utterance in utterances) async def process(service): """Parse text into commands.""" @@ -146,39 +147,6 @@ async def async_setup(hass, config): return True -def _create_matcher(utterance): - """Create a regex that matches the utterance.""" - # Split utterance into parts that are type: NORMAL, GROUP or OPTIONAL - # Pattern matches (GROUP|OPTIONAL): Change light to [the color] {name} - parts = re.split(r'({\w+}|\[[\w\s]+\] *)', utterance) - # Pattern to extract name from GROUP part. Matches {name} - group_matcher = re.compile(r'{(\w+)}') - # Pattern to extract text from OPTIONAL part. Matches [the color] - optional_matcher = re.compile(r'\[([\w ]+)\] *') - - pattern = ['^'] - for part in parts: - group_match = group_matcher.match(part) - optional_match = optional_matcher.match(part) - - # Normal part - if group_match is None and optional_match is None: - pattern.append(part) - continue - - # Group part - if group_match is not None: - pattern.append( - r'(?P<{}>[\w ]+?)\s*'.format(group_match.groups()[0])) - - # Optional part - elif optional_match is not None: - pattern.append(r'(?:{} *)?'.format(optional_match.groups()[0])) - - pattern.append('$') - return re.compile(''.join(pattern), re.I) - - async def _process(hass, text): """Process a line of text.""" intents = hass.data.get(DOMAIN, {}) diff --git a/homeassistant/components/conversation/util.py b/homeassistant/components/conversation/util.py new file mode 100644 index 00000000000..60d861afdbe --- /dev/null +++ b/homeassistant/components/conversation/util.py @@ -0,0 +1,35 @@ +"""Util for Conversation.""" +import re + + +def create_matcher(utterance): + """Create a regex that matches the utterance.""" + # Split utterance into parts that are type: NORMAL, GROUP or OPTIONAL + # Pattern matches (GROUP|OPTIONAL): Change light to [the color] {name} + parts = re.split(r'({\w+}|\[[\w\s]+\] *)', utterance) + # Pattern to extract name from GROUP part. Matches {name} + group_matcher = re.compile(r'{(\w+)}') + # Pattern to extract text from OPTIONAL part. Matches [the color] + optional_matcher = re.compile(r'\[([\w ]+)\] *') + + pattern = ['^'] + for part in parts: + group_match = group_matcher.match(part) + optional_match = optional_matcher.match(part) + + # Normal part + if group_match is None and optional_match is None: + pattern.append(part) + continue + + # Group part + if group_match is not None: + pattern.append( + r'(?P<{}>[\w ]+?)\s*'.format(group_match.groups()[0])) + + # Optional part + elif optional_match is not None: + pattern.append(r'(?:{} *)?'.format(optional_match.groups()[0])) + + pattern.append('$') + return re.compile(''.join(pattern), re.I) diff --git a/homeassistant/components/hangouts/__init__.py b/homeassistant/components/hangouts/__init__.py index 8ebacc3736b..72a7e015a22 100644 --- a/homeassistant/components/hangouts/__init__.py +++ b/homeassistant/components/hangouts/__init__.py @@ -11,28 +11,56 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import dispatcher +import homeassistant.helpers.config_validation as cv -from .config_flow import configured_hangouts from .const import ( - CONF_BOT, CONF_COMMANDS, CONF_REFRESH_TOKEN, DOMAIN, + CONF_BOT, CONF_INTENTS, CONF_REFRESH_TOKEN, DOMAIN, EVENT_HANGOUTS_CONNECTED, EVENT_HANGOUTS_CONVERSATIONS_CHANGED, MESSAGE_SCHEMA, SERVICE_SEND_MESSAGE, - SERVICE_UPDATE) + SERVICE_UPDATE, CONF_SENTENCES, CONF_MATCHERS, + CONF_ERROR_SUPPRESSED_CONVERSATIONS, INTENT_SCHEMA, TARGETS_SCHEMA) + +# We need an import from .config_flow, without it .config_flow is never loaded. +from .config_flow import HangoutsFlowHandler # noqa: F401 + REQUIREMENTS = ['hangups==0.4.5'] _LOGGER = logging.getLogger(__name__) +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_INTENTS, default={}): vol.Schema({ + cv.string: INTENT_SCHEMA + }), + vol.Optional(CONF_ERROR_SUPPRESSED_CONVERSATIONS, default=[]): + [TARGETS_SCHEMA] + }) +}, extra=vol.ALLOW_EXTRA) + async def async_setup(hass, config): """Set up the Hangouts bot component.""" - config = config.get(DOMAIN, {}) - hass.data[DOMAIN] = {CONF_COMMANDS: config.get(CONF_COMMANDS, [])} + from homeassistant.components.conversation import create_matcher - if configured_hangouts(hass) is None: - hass.async_add_job(hass.config_entries.flow.async_init( - DOMAIN, context={'source': config_entries.SOURCE_IMPORT} - )) + config = config.get(DOMAIN) + if config is None: + return True + + hass.data[DOMAIN] = {CONF_INTENTS: config.get(CONF_INTENTS), + CONF_ERROR_SUPPRESSED_CONVERSATIONS: + config.get(CONF_ERROR_SUPPRESSED_CONVERSATIONS)} + + for data in hass.data[DOMAIN][CONF_INTENTS].values(): + matchers = [] + for sentence in data[CONF_SENTENCES]: + matchers.append(create_matcher(sentence)) + + data[CONF_MATCHERS] = matchers + + hass.async_add_job(hass.config_entries.flow.async_init( + DOMAIN, context={'source': config_entries.SOURCE_IMPORT} + )) return True @@ -47,7 +75,8 @@ async def async_setup_entry(hass, config): bot = HangoutsBot( hass, config.data.get(CONF_REFRESH_TOKEN), - hass.data[DOMAIN][CONF_COMMANDS]) + hass.data[DOMAIN][CONF_INTENTS], + hass.data[DOMAIN][CONF_ERROR_SUPPRESSED_CONVERSATIONS]) hass.data[DOMAIN][CONF_BOT] = bot except GoogleAuthError as exception: _LOGGER.error("Hangouts failed to log in: %s", str(exception)) @@ -62,6 +91,10 @@ async def async_setup_entry(hass, config): hass, EVENT_HANGOUTS_CONVERSATIONS_CHANGED, bot.async_update_conversation_commands) + dispatcher.async_dispatcher_connect( + hass, + EVENT_HANGOUTS_CONVERSATIONS_CHANGED, + bot.async_handle_update_error_suppressed_conversations) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, bot.async_handle_hass_stop) diff --git a/homeassistant/components/hangouts/const.py b/homeassistant/components/hangouts/const.py index 7083307f3e2..3b96edf93a2 100644 --- a/homeassistant/components/hangouts/const.py +++ b/homeassistant/components/hangouts/const.py @@ -4,7 +4,6 @@ import logging import voluptuous as vol from homeassistant.components.notify import ATTR_MESSAGE, ATTR_TARGET -from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger('homeassistant.components.hangouts') @@ -18,17 +17,18 @@ CONF_BOT = 'bot' CONF_CONVERSATIONS = 'conversations' CONF_DEFAULT_CONVERSATIONS = 'default_conversations' +CONF_ERROR_SUPPRESSED_CONVERSATIONS = 'error_suppressed_conversations' -CONF_COMMANDS = 'commands' -CONF_WORD = 'word' -CONF_EXPRESSION = 'expression' - -EVENT_HANGOUTS_COMMAND = 'hangouts_command' +CONF_INTENTS = 'intents' +CONF_INTENT_TYPE = 'intent_type' +CONF_SENTENCES = 'sentences' +CONF_MATCHERS = 'matchers' EVENT_HANGOUTS_CONNECTED = 'hangouts_connected' EVENT_HANGOUTS_DISCONNECTED = 'hangouts_disconnected' EVENT_HANGOUTS_USERS_CHANGED = 'hangouts_users_changed' EVENT_HANGOUTS_CONVERSATIONS_CHANGED = 'hangouts_conversations_changed' +EVENT_HANGOUTS_MESSAGE_RECEIVED = 'hangouts_message_received' CONF_CONVERSATION_ID = 'id' CONF_CONVERSATION_NAME = 'name' @@ -59,20 +59,10 @@ MESSAGE_SCHEMA = vol.Schema({ vol.Required(ATTR_MESSAGE): [MESSAGE_SEGMENT_SCHEMA] }) -COMMAND_SCHEMA = vol.All( +INTENT_SCHEMA = vol.All( # Basic Schema vol.Schema({ - vol.Exclusive(CONF_WORD, 'trigger'): cv.string, - vol.Exclusive(CONF_EXPRESSION, 'trigger'): cv.is_regex, - vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_SENTENCES): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_CONVERSATIONS): [TARGETS_SCHEMA] }), - # Make sure it's either a word or an expression command - cv.has_at_least_one_key(CONF_WORD, CONF_EXPRESSION) ) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_COMMANDS, default=[]): [COMMAND_SCHEMA] - }) -}, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index d9ffb4cbace..15f4156d374 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -1,13 +1,14 @@ """The Hangouts Bot.""" import logging -import re -from homeassistant.helpers import dispatcher +from homeassistant.helpers import dispatcher, intent from .const import ( - ATTR_MESSAGE, ATTR_TARGET, CONF_CONVERSATIONS, CONF_EXPRESSION, CONF_NAME, - CONF_WORD, DOMAIN, EVENT_HANGOUTS_COMMAND, EVENT_HANGOUTS_CONNECTED, - EVENT_HANGOUTS_CONVERSATIONS_CHANGED, EVENT_HANGOUTS_DISCONNECTED) + ATTR_MESSAGE, ATTR_TARGET, CONF_CONVERSATIONS, DOMAIN, + EVENT_HANGOUTS_CONNECTED, EVENT_HANGOUTS_CONVERSATIONS_CHANGED, + EVENT_HANGOUTS_DISCONNECTED, EVENT_HANGOUTS_MESSAGE_RECEIVED, + CONF_MATCHERS, CONF_CONVERSATION_ID, + CONF_CONVERSATION_NAME) _LOGGER = logging.getLogger(__name__) @@ -15,20 +16,34 @@ _LOGGER = logging.getLogger(__name__) class HangoutsBot: """The Hangouts Bot.""" - def __init__(self, hass, refresh_token, commands): + def __init__(self, hass, refresh_token, intents, error_suppressed_convs): """Set up the client.""" self.hass = hass self._connected = False self._refresh_token = refresh_token - self._commands = commands + self._intents = intents + self._conversation_intents = None - self._word_commands = None - self._expression_commands = None self._client = None self._user_list = None self._conversation_list = None + self._error_suppressed_convs = error_suppressed_convs + self._error_suppressed_conv_ids = None + + dispatcher.async_dispatcher_connect( + self.hass, EVENT_HANGOUTS_MESSAGE_RECEIVED, + self._async_handle_conversation_message) + + def _resolve_conversation_id(self, obj): + if CONF_CONVERSATION_ID in obj: + return obj[CONF_CONVERSATION_ID] + if CONF_CONVERSATION_NAME in obj: + conv = self._resolve_conversation_name(obj[CONF_CONVERSATION_NAME]) + if conv is not None: + return conv.id_ + return None def _resolve_conversation_name(self, name): for conv in self._conversation_list.get_all(): @@ -38,89 +53,100 @@ class HangoutsBot: def async_update_conversation_commands(self, _): """Refresh the commands for every conversation.""" - self._word_commands = {} - self._expression_commands = {} + self._conversation_intents = {} - for command in self._commands: - if command.get(CONF_CONVERSATIONS): + for intent_type, data in self._intents.items(): + if data.get(CONF_CONVERSATIONS): conversations = [] - for conversation in command.get(CONF_CONVERSATIONS): - if 'id' in conversation: - conversations.append(conversation['id']) - elif 'name' in conversation: - conversations.append(self._resolve_conversation_name( - conversation['name']).id_) - command['_' + CONF_CONVERSATIONS] = conversations + for conversation in data.get(CONF_CONVERSATIONS): + conv_id = self._resolve_conversation_id(conversation) + if conv_id is not None: + conversations.append(conv_id) + data['_' + CONF_CONVERSATIONS] = conversations else: - command['_' + CONF_CONVERSATIONS] = \ + data['_' + CONF_CONVERSATIONS] = \ [conv.id_ for conv in self._conversation_list.get_all()] - if command.get(CONF_WORD): - for conv_id in command['_' + CONF_CONVERSATIONS]: - if conv_id not in self._word_commands: - self._word_commands[conv_id] = {} - word = command[CONF_WORD].lower() - self._word_commands[conv_id][word] = command - elif command.get(CONF_EXPRESSION): - command['_' + CONF_EXPRESSION] = re.compile( - command.get(CONF_EXPRESSION)) + for conv_id in data['_' + CONF_CONVERSATIONS]: + if conv_id not in self._conversation_intents: + self._conversation_intents[conv_id] = {} - for conv_id in command['_' + CONF_CONVERSATIONS]: - if conv_id not in self._expression_commands: - self._expression_commands[conv_id] = [] - self._expression_commands[conv_id].append(command) + self._conversation_intents[conv_id][intent_type] = data try: self._conversation_list.on_event.remove_observer( - self._handle_conversation_event) + self._async_handle_conversation_event) except ValueError: pass self._conversation_list.on_event.add_observer( - self._handle_conversation_event) + self._async_handle_conversation_event) - def _handle_conversation_event(self, event): + def async_handle_update_error_suppressed_conversations(self, _): + """Resolve the list of error suppressed conversations.""" + self._error_suppressed_conv_ids = [] + for conversation in self._error_suppressed_convs: + conv_id = self._resolve_conversation_id(conversation) + if conv_id is not None: + self._error_suppressed_conv_ids.append(conv_id) + + async def _async_handle_conversation_event(self, event): from hangups import ChatMessageEvent - if event.__class__ is ChatMessageEvent: - self._handle_conversation_message( - event.conversation_id, event.user_id, event) + if isinstance(event, ChatMessageEvent): + dispatcher.async_dispatcher_send(self.hass, + EVENT_HANGOUTS_MESSAGE_RECEIVED, + event.conversation_id, + event.user_id, event) - def _handle_conversation_message(self, conv_id, user_id, event): + async def _async_handle_conversation_message(self, + conv_id, user_id, event): """Handle a message sent to a conversation.""" user = self._user_list.get_user(user_id) if user.is_self: return + message = event.text _LOGGER.debug("Handling message '%s' from %s", - event.text, user.full_name) + message, user.full_name) - event_data = None + intents = self._conversation_intents.get(conv_id) + if intents is not None: + is_error = False + try: + intent_result = await self._async_process(intents, message) + except (intent.UnknownIntent, intent.IntentHandleError) as err: + is_error = True + intent_result = intent.IntentResponse() + intent_result.async_set_speech(str(err)) + + if intent_result is None: + is_error = True + intent_result = intent.IntentResponse() + intent_result.async_set_speech( + "Sorry, I didn't understand that") + + message = intent_result.as_dict().get('speech', {})\ + .get('plain', {}).get('speech') + + if (message is not None) and not ( + is_error and conv_id in self._error_suppressed_conv_ids): + await self._async_send_message( + [{'text': message, 'parse_str': True}], + [{CONF_CONVERSATION_ID: conv_id}]) + + async def _async_process(self, intents, text): + """Detect a matching intent.""" + for intent_type, data in intents.items(): + for matcher in data.get(CONF_MATCHERS, []): + match = matcher.match(text) - pieces = event.text.split(' ') - cmd = pieces[0].lower() - command = self._word_commands.get(conv_id, {}).get(cmd) - if command: - event_data = { - 'command': command[CONF_NAME], - 'conversation_id': conv_id, - 'user_id': user_id, - 'user_name': user.full_name, - 'data': pieces[1:] - } - else: - # After single-word commands, check all regex commands in the room - for command in self._expression_commands.get(conv_id, []): - match = command['_' + CONF_EXPRESSION].match(event.text) if not match: continue - event_data = { - 'command': command[CONF_NAME], - 'conversation_id': conv_id, - 'user_id': user_id, - 'user_name': user.full_name, - 'data': match.groupdict() - } - if event_data is not None: - self.hass.bus.fire(EVENT_HANGOUTS_COMMAND, event_data) + + response = await self.hass.helpers.intent.async_handle( + DOMAIN, intent_type, + {key: {'value': value} for key, value + in match.groupdict().items()}, text) + return response async def async_connect(self): """Login to the Google Hangouts.""" @@ -163,10 +189,12 @@ class HangoutsBot: conversations = [] for target in targets: conversation = None - if 'id' in target: - conversation = self._conversation_list.get(target['id']) - elif 'name' in target: - conversation = self._resolve_conversation_name(target['name']) + if CONF_CONVERSATION_ID in target: + conversation = self._conversation_list.get( + target[CONF_CONVERSATION_ID]) + elif CONF_CONVERSATION_NAME in target: + conversation = self._resolve_conversation_name( + target[CONF_CONVERSATION_NAME]) if conversation is not None: conversations.append(conversation) @@ -200,8 +228,8 @@ class HangoutsBot: users_in_conversation = [] for user in conv.users: users_in_conversation.append(user.full_name) - conversations[str(i)] = {'id': str(conv.id_), - 'name': conv.name, + conversations[str(i)] = {CONF_CONVERSATION_ID: str(conv.id_), + CONF_CONVERSATION_NAME: conv.name, 'users': users_in_conversation} self.hass.states.async_set("{}.conversations".format(DOMAIN), diff --git a/tests/components/test_conversation.py b/tests/components/test_conversation.py index 6a1d5a55c47..61247b5bdde 100644 --- a/tests/components/test_conversation.py +++ b/tests/components/test_conversation.py @@ -290,11 +290,11 @@ async def test_http_api_wrong_data(hass, aiohttp_client): def test_create_matcher(): """Test the create matcher method.""" # Basic sentence - pattern = conversation._create_matcher('Hello world') + pattern = conversation.create_matcher('Hello world') assert pattern.match('Hello world') is not None # Match a part - pattern = conversation._create_matcher('Hello {name}') + pattern = conversation.create_matcher('Hello {name}') match = pattern.match('hello world') assert match is not None assert match.groupdict()['name'] == 'world' @@ -302,7 +302,7 @@ def test_create_matcher(): assert no_match is None # Optional and matching part - pattern = conversation._create_matcher('Turn on [the] {name}') + pattern = conversation.create_matcher('Turn on [the] {name}') match = pattern.match('turn on the kitchen lights') assert match is not None assert match.groupdict()['name'] == 'kitchen lights' @@ -313,7 +313,7 @@ def test_create_matcher(): assert match is None # Two different optional parts, 1 matching part - pattern = conversation._create_matcher('Turn on [the] [a] {name}') + pattern = conversation.create_matcher('Turn on [the] [a] {name}') match = pattern.match('turn on the kitchen lights') assert match is not None assert match.groupdict()['name'] == 'kitchen lights' @@ -325,13 +325,13 @@ def test_create_matcher(): assert match.groupdict()['name'] == 'kitchen light' # Strip plural - pattern = conversation._create_matcher('Turn {name}[s] on') + pattern = conversation.create_matcher('Turn {name}[s] on') match = pattern.match('turn kitchen lights on') assert match is not None assert match.groupdict()['name'] == 'kitchen light' # Optional 2 words - pattern = conversation._create_matcher('Turn [the great] {name} on') + pattern = conversation.create_matcher('Turn [the great] {name} on') match = pattern.match('turn the great kitchen lights on') assert match is not None assert match.groupdict()['name'] == 'kitchen lights' From 8ab31fe13939278cb9f6237cb22b2eb485d75066 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 28 Aug 2018 00:37:04 +0200 Subject: [PATCH 039/172] Store devices as dict instead of list (#16229) * Store devices as dict instead of list * Use OrderedDict --- homeassistant/helpers/device_registry.py | 14 ++++++++------ tests/helpers/test_device_registry.py | 4 +++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 31da40134a5..504448b948d 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -2,6 +2,8 @@ import logging import uuid +from collections import OrderedDict + import attr from homeassistant.core import callback @@ -45,7 +47,7 @@ class DeviceRegistry: @callback def async_get_device(self, identifiers: str, connections: tuple): """Check if device is registered.""" - for device in self.devices: + for device in self.devices.values(): if any(iden in device.identifiers for iden in identifiers) or \ any(conn in device.connections for conn in connections): return device @@ -75,7 +77,7 @@ class DeviceRegistry: name=name, sw_version=sw_version ) - self.devices.append(device) + self.devices[device.id] = device self.async_schedule_save() @@ -86,10 +88,10 @@ class DeviceRegistry: devices = await self._store.async_load() if devices is None: - self.devices = [] + self.devices = OrderedDict() return - self.devices = [DeviceEntry( + self.devices = {device['id']: DeviceEntry( config_entries=device['config_entries'], connections={tuple(conn) for conn in device['connections']}, identifiers={tuple(iden) for iden in device['identifiers']}, @@ -98,7 +100,7 @@ class DeviceRegistry: name=device['name'], sw_version=device['sw_version'], id=device['id'], - ) for device in devices['devices']] + ) for device in devices['devices']} @callback def async_schedule_save(self): @@ -120,7 +122,7 @@ class DeviceRegistry: 'name': entry.name, 'sw_version': entry.sw_version, 'id': entry.id, - } for entry in self.devices + } for entry in self.devices.values() ] return data diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index b2e73071823..84ad54f7b82 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -1,13 +1,15 @@ """Tests for the Device Registry.""" import pytest +from collections import OrderedDict + from homeassistant.helpers import device_registry def mock_registry(hass, mock_entries=None): """Mock the Device Registry.""" registry = device_registry.DeviceRegistry(hass) - registry.devices = mock_entries or [] + registry.devices = mock_entries or OrderedDict() async def _get_reg(): return registry From 5397c0d73a7ee02f357a8c4d2ef4c942968bc39f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 28 Aug 2018 00:37:15 +0200 Subject: [PATCH 040/172] Update trusted networks flow (#16227) * Update the trusted networks flow * Fix tests * Remove errors --- .../auth/providers/trusted_networks.py | 20 ++++--------------- tests/auth/providers/test_trusted_networks.py | 13 ++++-------- 2 files changed, 8 insertions(+), 25 deletions(-) diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index 37e032e58d7..8a7e1d67c6d 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -111,31 +111,19 @@ class TrustedNetworksLoginFlow(LoginFlow): self, user_input: Optional[Dict[str, str]] = None) \ -> Dict[str, Any]: """Handle the step of the form.""" - errors = {} try: cast(TrustedNetworksAuthProvider, self._auth_provider)\ .async_validate_access(self._ip_address) except InvalidAuthError: - errors['base'] = 'invalid_auth' - return self.async_show_form( - step_id='init', - data_schema=None, - errors=errors, + return self.async_abort( + reason='not_whitelisted' ) if user_input is not None: - user_id = user_input['user'] - if user_id not in self._available_users: - errors['base'] = 'invalid_auth' - - if not errors: - return await self.async_finish(user_input) - - schema = {'user': vol.In(self._available_users)} + return await self.async_finish(user_input) return self.async_show_form( step_id='init', - data_schema=vol.Schema(schema), - errors=errors, + data_schema=vol.Schema({'user': vol.In(self._available_users)}), ) diff --git a/tests/auth/providers/test_trusted_networks.py b/tests/auth/providers/test_trusted_networks.py index 4839c72a86a..0ca302f8273 100644 --- a/tests/auth/providers/test_trusted_networks.py +++ b/tests/auth/providers/test_trusted_networks.py @@ -74,16 +74,16 @@ async def test_login_flow(manager, provider): # trusted network didn't loaded flow = await provider.async_login_flow({'ip_address': '127.0.0.1'}) step = await flow.async_step_init() - assert step['step_id'] == 'init' - assert step['errors']['base'] == 'invalid_auth' + assert step['type'] == 'abort' + assert step['reason'] == 'not_whitelisted' provider.hass.http = Mock(trusted_networks=['192.168.0.1']) # not from trusted network flow = await provider.async_login_flow({'ip_address': '127.0.0.1'}) step = await flow.async_step_init() - assert step['step_id'] == 'init' - assert step['errors']['base'] == 'invalid_auth' + assert step['type'] == 'abort' + assert step['reason'] == 'not_whitelisted' # from trusted network, list users flow = await provider.async_login_flow({'ip_address': '192.168.0.1'}) @@ -95,11 +95,6 @@ async def test_login_flow(manager, provider): with pytest.raises(vol.Invalid): assert schema({'user': 'invalid-user'}) - # login with invalid user - step = await flow.async_step_init({'user': 'invalid-user'}) - assert step['step_id'] == 'init' - assert step['errors']['base'] == 'invalid_auth' - # login with valid user step = await flow.async_step_init({'user': user.id}) assert step['type'] == 'create_entry' From 376d4e4fa0bbcbfa07646f49f9d8fd56c8c0df3c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 28 Aug 2018 09:32:50 +0200 Subject: [PATCH 041/172] Warning missed a space (#16233) --- homeassistant/components/http/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 6909a0e4664..1b22f8e62d4 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -202,7 +202,7 @@ class HomeAssistantHTTP: if hass.auth.active and hass.auth.support_legacy: _LOGGER.warning( - "legacy_api_password support has been enabled. If you don't" + "legacy_api_password support has been enabled. If you don't " "require it, remove the 'api_password' from your http config.") setup_auth(app, trusted_networks, hass.auth.active, From a14980716d0752b6c0367fe2eee06369e8c70168 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 28 Aug 2018 10:53:12 +0200 Subject: [PATCH 042/172] Package loadable: compare case insensitive (#16234) --- homeassistant/util/package.py | 4 +++- tests/util/test_package.py | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index feefa65c0f6..3f12fc223b8 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -73,11 +73,13 @@ def package_loadable(package: str) -> bool: # This is a zip file req = pkg_resources.Requirement.parse(urlparse(package).fragment) + req_proj_name = req.project_name.lower() + for path in sys.path: for dist in pkg_resources.find_distributions(path): # If the project name is the same, it will be the one that is # loaded when we import it. - if dist.project_name == req.project_name: + if dist.project_name.lower() == req_proj_name: return dist in req return False diff --git a/tests/util/test_package.py b/tests/util/test_package.py index 19e85a094ee..1e93a078bd9 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -239,3 +239,6 @@ def test_package_loadable_installed_twice(): with patch('pkg_resources.find_distributions', side_effect=[[v2]]): assert package.package_loadable('hello==2.0.0') + + with patch('pkg_resources.find_distributions', side_effect=[[v2]]): + assert package.package_loadable('Hello==2.0.0') From 67df162bcc1d9eb84dfbc43e108cad4a81d67130 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Tue, 28 Aug 2018 02:23:58 -0700 Subject: [PATCH 043/172] Change log level to error when auth provider failed loading (#16235) --- homeassistant/auth/mfa_modules/__init__.py | 4 ++-- homeassistant/auth/providers/__init__.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index cb0758e3ef8..a669f8bb5f0 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -152,8 +152,8 @@ async def _load_mfa_module(hass: HomeAssistant, module_name: str) \ try: module = importlib.import_module(module_path) - except ImportError: - _LOGGER.warning('Unable to find %s', module_path) + except ImportError as err: + _LOGGER.error('Unable to load mfa module %s: %s', module_name, err) return None if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'): diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index 0bcb47d4af9..d8ec04e9072 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -134,8 +134,8 @@ async def load_auth_provider_module( try: module = importlib.import_module( 'homeassistant.auth.providers.{}'.format(provider)) - except ImportError: - _LOGGER.warning('Unable to find auth provider %s', provider) + except ImportError as err: + _LOGGER.error('Unable to load auth provider %s: %s', provider, err) return None if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'): From 12709ceaa3b4f3151a670ae878898687608863bb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 28 Aug 2018 12:49:50 +0200 Subject: [PATCH 044/172] Avoid insecure pycryptodome (#16238) --- homeassistant/package_constraints.txt | 2 ++ script/gen_requirements_all.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 70fb519eef4..3e9a763181a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,6 +13,8 @@ pyyaml>=3.13,<4 requests==2.19.1 voluptuous==0.11.5 +pycryptodome>=3.6.6 + # Breaks Python 3.6 and is not needed for our supported Python versions enum34==1000000000.0.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index fe23e638e5b..4b694ec7ec0 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -124,6 +124,8 @@ URL_PIN = ('https://home-assistant.io/developers/code_review_platform/' CONSTRAINT_PATH = os.path.join(os.path.dirname(__file__), '../homeassistant/package_constraints.txt') CONSTRAINT_BASE = """ +pycryptodome>=3.6.6 + # Breaks Python 3.6 and is not needed for our supported Python versions enum34==1000000000.0.0 From 09dc4d663d3e3189156c38a592f96ee98ecbb8d7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 28 Aug 2018 12:52:18 +0200 Subject: [PATCH 045/172] Improve package loadable (#16237) * Add caching to package loadable * Fix tests * Improve package loadable * Lint * Typing --- homeassistant/requirements.py | 60 +++++++++++++++++++++++ homeassistant/util/package.py | 80 +++++++++--------------------- tests/test_requirements.py | 92 +++++++++++++++++++++++++++++------ tests/util/test_package.py | 78 +++-------------------------- 4 files changed, 166 insertions(+), 144 deletions(-) diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index b9b5e137d5c..a3d168d22e7 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -3,12 +3,17 @@ import asyncio from functools import partial import logging import os +import sys from typing import Any, Dict, List, Optional +from urllib.parse import urlparse + +import pkg_resources import homeassistant.util.package as pkg_util from homeassistant.core import HomeAssistant DATA_PIP_LOCK = 'pip_lock' +DATA_PKG_CACHE = 'pkg_cache' CONSTRAINT_FILE = 'package_constraints.txt' _LOGGER = logging.getLogger(__name__) @@ -23,12 +28,20 @@ async def async_process_requirements(hass: HomeAssistant, name: str, if pip_lock is None: pip_lock = hass.data[DATA_PIP_LOCK] = asyncio.Lock(loop=hass.loop) + pkg_cache = hass.data.get(DATA_PKG_CACHE) + if pkg_cache is None: + pkg_cache = hass.data[DATA_PKG_CACHE] = PackageLoadable(hass) + pip_install = partial(pkg_util.install_package, **pip_kwargs(hass.config.config_dir)) async with pip_lock: for req in requirements: + if await pkg_cache.loadable(req): + continue + ret = await hass.async_add_executor_job(pip_install, req) + if not ret: _LOGGER.error("Not initializing %s because could not install " "requirement %s", name, req) @@ -45,3 +58,50 @@ def pip_kwargs(config_dir: Optional[str]) -> Dict[str, Any]: if not (config_dir is None or pkg_util.is_virtual_env()): kwargs['target'] = os.path.join(config_dir, 'deps') return kwargs + + +class PackageLoadable: + """Class to check if a package is loadable, with built-in cache.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the PackageLoadable class.""" + self.dist_cache = {} # type: Dict[str, pkg_resources.Distribution] + self.hass = hass + + async def loadable(self, package: str) -> bool: + """Check if a package is what will be loaded when we import it. + + Returns True when the requirement is met. + Returns False when the package is not installed or doesn't meet req. + """ + dist_cache = self.dist_cache + + try: + req = pkg_resources.Requirement.parse(package) + except ValueError: + # This is a zip file. We no longer use this in Home Assistant, + # leaving it in for custom components. + req = pkg_resources.Requirement.parse(urlparse(package).fragment) + + req_proj_name = req.project_name.lower() + dist = dist_cache.get(req_proj_name) + + if dist is not None: + return dist in req + + for path in sys.path: + # We read the whole mount point as we're already here + # Caching it on first call makes subsequent calls a lot faster. + await self.hass.async_add_executor_job(self._fill_cache, path) + + dist = dist_cache.get(req_proj_name) + if dist is not None: + return dist in req + + return False + + def _fill_cache(self, path: str) -> None: + """Add packages from a path to the cache.""" + dist_cache = self.dist_cache + for dist in pkg_resources.find_distributions(path): + dist_cache.setdefault(dist.project_name.lower(), dist) diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index 3f12fc223b8..422809f7594 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -4,17 +4,11 @@ import logging import os from subprocess import PIPE, Popen import sys -import threading -from urllib.parse import urlparse from typing import Optional -import pkg_resources - _LOGGER = logging.getLogger(__name__) -INSTALL_LOCK = threading.Lock() - def is_virtual_env() -> bool: """Return if we run in a virtual environtment.""" @@ -31,58 +25,30 @@ def install_package(package: str, upgrade: bool = True, Return boolean if install successful. """ # Not using 'import pip; pip.main([])' because it breaks the logger - with INSTALL_LOCK: - if package_loadable(package): - return True + _LOGGER.info('Attempting install of %s', package) + env = os.environ.copy() + args = [sys.executable, '-m', 'pip', 'install', '--quiet', package] + if upgrade: + args.append('--upgrade') + if constraints is not None: + args += ['--constraint', constraints] + if target: + assert not is_virtual_env() + # This only works if not running in venv + args += ['--user'] + env['PYTHONUSERBASE'] = os.path.abspath(target) + if sys.platform != 'win32': + # Workaround for incompatible prefix setting + # See http://stackoverflow.com/a/4495175 + args += ['--prefix='] + process = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) + _, stderr = process.communicate() + if process.returncode != 0: + _LOGGER.error("Unable to install package %s: %s", + package, stderr.decode('utf-8').lstrip().strip()) + return False - _LOGGER.info('Attempting install of %s', package) - env = os.environ.copy() - args = [sys.executable, '-m', 'pip', 'install', '--quiet', package] - if upgrade: - args.append('--upgrade') - if constraints is not None: - args += ['--constraint', constraints] - if target: - assert not is_virtual_env() - # This only works if not running in venv - args += ['--user'] - env['PYTHONUSERBASE'] = os.path.abspath(target) - if sys.platform != 'win32': - # Workaround for incompatible prefix setting - # See http://stackoverflow.com/a/4495175 - args += ['--prefix='] - process = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) - _, stderr = process.communicate() - if process.returncode != 0: - _LOGGER.error("Unable to install package %s: %s", - package, stderr.decode('utf-8').lstrip().strip()) - return False - - return True - - -def package_loadable(package: str) -> bool: - """Check if a package is what will be loaded when we import it. - - Returns True when the requirement is met. - Returns False when the package is not installed or doesn't meet req. - """ - try: - req = pkg_resources.Requirement.parse(package) - except ValueError: - # This is a zip file - req = pkg_resources.Requirement.parse(urlparse(package).fragment) - - req_proj_name = req.project_name.lower() - - for path in sys.path: - for dist in pkg_resources.find_distributions(path): - # If the project name is the same, it will be the one that is - # loaded when we import it. - if dist.project_name.lower() == req_proj_name: - return dist in req - - return False + return True async def async_get_user_site(deps_dir: str) -> str: diff --git a/tests/test_requirements.py b/tests/test_requirements.py index e3ef797df4d..71ae80f22e4 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -1,11 +1,22 @@ """Test requirements module.""" import os -from unittest import mock +from unittest.mock import patch, call from homeassistant import loader, setup -from homeassistant.requirements import CONSTRAINT_FILE +from homeassistant.requirements import ( + CONSTRAINT_FILE, PackageLoadable, async_process_requirements) -from tests.common import get_test_home_assistant, MockModule +import pkg_resources + +from tests.common import get_test_home_assistant, MockModule, mock_coro + +RESOURCE_DIR = os.path.abspath( + os.path.join(os.path.dirname(__file__), '..', 'resources')) + +TEST_NEW_REQ = 'pyhelloworld3==1.0.0' + +TEST_ZIP_REQ = 'file://{}#{}' \ + .format(os.path.join(RESOURCE_DIR, 'pyhelloworld3.zip'), TEST_NEW_REQ) class TestRequirements: @@ -23,11 +34,9 @@ class TestRequirements: """Clean up.""" self.hass.stop() - @mock.patch('os.path.dirname') - @mock.patch('homeassistant.util.package.is_virtual_env', - return_value=True) - @mock.patch('homeassistant.util.package.install_package', - return_value=True) + @patch('os.path.dirname') + @patch('homeassistant.util.package.is_virtual_env', return_value=True) + @patch('homeassistant.util.package.install_package', return_value=True) def test_requirement_installed_in_venv( self, mock_install, mock_venv, mock_dirname): """Test requirement installed in virtual environment.""" @@ -39,15 +48,13 @@ class TestRequirements: MockModule('comp', requirements=['package==0.0.1'])) assert setup.setup_component(self.hass, 'comp') assert 'comp' in self.hass.config.components - assert mock_install.call_args == mock.call( + assert mock_install.call_args == call( 'package==0.0.1', constraints=os.path.join('ha_package_path', CONSTRAINT_FILE)) - @mock.patch('os.path.dirname') - @mock.patch('homeassistant.util.package.is_virtual_env', - return_value=False) - @mock.patch('homeassistant.util.package.install_package', - return_value=True) + @patch('os.path.dirname') + @patch('homeassistant.util.package.is_virtual_env', return_value=False) + @patch('homeassistant.util.package.install_package', return_value=True) def test_requirement_installed_in_deps( self, mock_install, mock_venv, mock_dirname): """Test requirement installed in deps directory.""" @@ -58,6 +65,61 @@ class TestRequirements: MockModule('comp', requirements=['package==0.0.1'])) assert setup.setup_component(self.hass, 'comp') assert 'comp' in self.hass.config.components - assert mock_install.call_args == mock.call( + assert mock_install.call_args == call( 'package==0.0.1', target=self.hass.config.path('deps'), constraints=os.path.join('ha_package_path', CONSTRAINT_FILE)) + + +async def test_install_existing_package(hass): + """Test an install attempt on an existing package.""" + with patch('homeassistant.util.package.install_package', + return_value=mock_coro(True)) as mock_inst: + assert await async_process_requirements( + hass, 'test_component', ['hello==1.0.0']) + + assert len(mock_inst.mock_calls) == 1 + + with patch('homeassistant.requirements.PackageLoadable.loadable', + return_value=mock_coro(True)), \ + patch( + 'homeassistant.util.package.install_package') as mock_inst: + assert await async_process_requirements( + hass, 'test_component', ['hello==1.0.0']) + + assert len(mock_inst.mock_calls) == 0 + + +async def test_check_package_global(hass): + """Test for an installed package.""" + installed_package = list(pkg_resources.working_set)[0].project_name + assert await PackageLoadable(hass).loadable(installed_package) + + +async def test_check_package_zip(hass): + """Test for an installed zip package.""" + assert not await PackageLoadable(hass).loadable(TEST_ZIP_REQ) + + +async def test_package_loadable_installed_twice(hass): + """Test that a package is loadable when installed twice. + + If a package is installed twice, only the first version will be imported. + Test that package_loadable will only compare with the first package. + """ + v1 = pkg_resources.Distribution(project_name='hello', version='1.0.0') + v2 = pkg_resources.Distribution(project_name='hello', version='2.0.0') + + with patch('pkg_resources.find_distributions', side_effect=[[v1]]): + assert not await PackageLoadable(hass).loadable('hello==2.0.0') + + with patch('pkg_resources.find_distributions', side_effect=[[v1], [v2]]): + assert not await PackageLoadable(hass).loadable('hello==2.0.0') + + with patch('pkg_resources.find_distributions', side_effect=[[v2], [v1]]): + assert await PackageLoadable(hass).loadable('hello==2.0.0') + + with patch('pkg_resources.find_distributions', side_effect=[[v2]]): + assert await PackageLoadable(hass).loadable('hello==2.0.0') + + with patch('pkg_resources.find_distributions', side_effect=[[v2]]): + assert await PackageLoadable(hass).loadable('Hello==2.0.0') diff --git a/tests/util/test_package.py b/tests/util/test_package.py index 1e93a078bd9..5422140c232 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -6,18 +6,12 @@ import sys from subprocess import PIPE from unittest.mock import MagicMock, call, patch -import pkg_resources import pytest import homeassistant.util.package as package -RESOURCE_DIR = os.path.abspath( - os.path.join(os.path.dirname(__file__), '..', 'resources')) -TEST_EXIST_REQ = 'pip>=7.0.0' TEST_NEW_REQ = 'pyhelloworld3==1.0.0' -TEST_ZIP_REQ = 'file://{}#{}' \ - .format(os.path.join(RESOURCE_DIR, 'pyhelloworld3.zip'), TEST_NEW_REQ) @pytest.fixture @@ -28,14 +22,6 @@ def mock_sys(): yield sys_mock -@pytest.fixture -def mock_exists(): - """Mock package_loadable.""" - with patch('homeassistant.util.package.package_loadable') as mock: - mock.return_value = False - yield mock - - @pytest.fixture def deps_dir(): """Return path to deps directory.""" @@ -89,20 +75,10 @@ def mock_async_subprocess(): return async_popen -def test_install_existing_package(mock_exists, mock_popen): - """Test an install attempt on an existing package.""" - mock_exists.return_value = True - assert package.install_package(TEST_EXIST_REQ) - assert mock_exists.call_count == 1 - assert mock_exists.call_args == call(TEST_EXIST_REQ) - assert mock_popen.return_value.communicate.call_count == 0 - - -def test_install(mock_sys, mock_exists, mock_popen, mock_env_copy, mock_venv): +def test_install(mock_sys, mock_popen, mock_env_copy, mock_venv): """Test an install attempt on a package that doesn't exist.""" env = mock_env_copy() assert package.install_package(TEST_NEW_REQ, False) - assert mock_exists.call_count == 1 assert mock_popen.call_count == 1 assert ( mock_popen.call_args == @@ -115,11 +91,10 @@ def test_install(mock_sys, mock_exists, mock_popen, mock_env_copy, mock_venv): def test_install_upgrade( - mock_sys, mock_exists, mock_popen, mock_env_copy, mock_venv): + mock_sys, mock_popen, mock_env_copy, mock_venv): """Test an upgrade attempt on a package.""" env = mock_env_copy() assert package.install_package(TEST_NEW_REQ) - assert mock_exists.call_count == 1 assert mock_popen.call_count == 1 assert ( mock_popen.call_args == @@ -131,8 +106,7 @@ def test_install_upgrade( assert mock_popen.return_value.communicate.call_count == 1 -def test_install_target( - mock_sys, mock_exists, mock_popen, mock_env_copy, mock_venv): +def test_install_target(mock_sys, mock_popen, mock_env_copy, mock_venv): """Test an install with a target.""" target = 'target_folder' env = mock_env_copy() @@ -144,7 +118,6 @@ def test_install_target( TEST_NEW_REQ, '--user', '--prefix='] assert package.install_package(TEST_NEW_REQ, False, target=target) - assert mock_exists.call_count == 1 assert mock_popen.call_count == 1 assert ( mock_popen.call_args == @@ -153,15 +126,14 @@ def test_install_target( assert mock_popen.return_value.communicate.call_count == 1 -def test_install_target_venv( - mock_sys, mock_exists, mock_popen, mock_env_copy, mock_venv): +def test_install_target_venv(mock_sys, mock_popen, mock_env_copy, mock_venv): """Test an install with a target in a virtual environment.""" target = 'target_folder' with pytest.raises(AssertionError): package.install_package(TEST_NEW_REQ, False, target=target) -def test_install_error(caplog, mock_sys, mock_exists, mock_popen, mock_venv): +def test_install_error(caplog, mock_sys, mock_popen, mock_venv): """Test an install with a target.""" caplog.set_level(logging.WARNING) mock_popen.return_value.returncode = 1 @@ -171,14 +143,12 @@ def test_install_error(caplog, mock_sys, mock_exists, mock_popen, mock_venv): assert record.levelname == 'ERROR' -def test_install_constraint( - mock_sys, mock_exists, mock_popen, mock_env_copy, mock_venv): +def test_install_constraint(mock_sys, mock_popen, mock_env_copy, mock_venv): """Test install with constraint file on not installed package.""" env = mock_env_copy() constraints = 'constraints_file.txt' assert package.install_package( TEST_NEW_REQ, False, constraints=constraints) - assert mock_exists.call_count == 1 assert mock_popen.call_count == 1 assert ( mock_popen.call_args == @@ -190,17 +160,6 @@ def test_install_constraint( assert mock_popen.return_value.communicate.call_count == 1 -def test_check_package_global(): - """Test for an installed package.""" - installed_package = list(pkg_resources.working_set)[0].project_name - assert package.package_loadable(installed_package) - - -def test_check_package_zip(): - """Test for an installed zip package.""" - assert not package.package_loadable(TEST_ZIP_REQ) - - @asyncio.coroutine def test_async_get_user_site(mock_env_copy): """Test async get user site directory.""" @@ -217,28 +176,3 @@ def test_async_get_user_site(mock_env_copy): stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL, env=env) assert ret == os.path.join(deps_dir, 'lib_dir') - - -def test_package_loadable_installed_twice(): - """Test that a package is loadable when installed twice. - - If a package is installed twice, only the first version will be imported. - Test that package_loadable will only compare with the first package. - """ - v1 = pkg_resources.Distribution(project_name='hello', version='1.0.0') - v2 = pkg_resources.Distribution(project_name='hello', version='2.0.0') - - with patch('pkg_resources.find_distributions', side_effect=[[v1]]): - assert not package.package_loadable('hello==2.0.0') - - with patch('pkg_resources.find_distributions', side_effect=[[v1], [v2]]): - assert not package.package_loadable('hello==2.0.0') - - with patch('pkg_resources.find_distributions', side_effect=[[v2], [v1]]): - assert package.package_loadable('hello==2.0.0') - - with patch('pkg_resources.find_distributions', side_effect=[[v2]]): - assert package.package_loadable('hello==2.0.0') - - with patch('pkg_resources.find_distributions', side_effect=[[v2]]): - assert package.package_loadable('Hello==2.0.0') From 9a786e449b2a4ad598ad80da57b3f664d14b4204 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 28 Aug 2018 15:44:06 +0200 Subject: [PATCH 046/172] Fix hangouts (#16232) --- homeassistant/components/hangouts/__init__.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hangouts/__init__.py b/homeassistant/components/hangouts/__init__.py index 72a7e015a22..ebadff57be3 100644 --- a/homeassistant/components/hangouts/__init__.py +++ b/homeassistant/components/hangouts/__init__.py @@ -45,11 +45,17 @@ async def async_setup(hass, config): config = config.get(DOMAIN) if config is None: + hass.data[DOMAIN] = { + CONF_INTENTS: {}, + CONF_ERROR_SUPPRESSED_CONVERSATIONS: [], + } return True - hass.data[DOMAIN] = {CONF_INTENTS: config.get(CONF_INTENTS), - CONF_ERROR_SUPPRESSED_CONVERSATIONS: - config.get(CONF_ERROR_SUPPRESSED_CONVERSATIONS)} + hass.data[DOMAIN] = { + CONF_INTENTS: config[CONF_INTENTS], + CONF_ERROR_SUPPRESSED_CONVERSATIONS: + config[CONF_ERROR_SUPPRESSED_CONVERSATIONS], + } for data in hass.data[DOMAIN][CONF_INTENTS].values(): matchers = [] @@ -58,7 +64,7 @@ async def async_setup(hass, config): data[CONF_MATCHERS] = matchers - hass.async_add_job(hass.config_entries.flow.async_init( + hass.async_create_task(hass.config_entries.flow.async_init( DOMAIN, context={'source': config_entries.SOURCE_IMPORT} )) From 257b8b9b8018ceeb50d5371d6b31bbb83d7bc2c5 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Tue, 28 Aug 2018 11:54:01 -0700 Subject: [PATCH 047/172] Blow up startup if init auth providers or modules failed (#16240) * Blow up startup if init auth providers or modules failed * Delete core.entity_registry --- homeassistant/auth/__init__.py | 25 ++------ homeassistant/auth/mfa_modules/__init__.py | 17 +++--- homeassistant/auth/providers/__init__.py | 17 +++--- homeassistant/bootstrap.py | 12 ++-- homeassistant/config.py | 56 +++++++++++++++-- tests/auth/providers/test_homeassistant.py | 11 ++-- tests/auth/test_init.py | 58 +++++++++++------- tests/test_config.py | 70 +++++++++++++++++++++- 8 files changed, 194 insertions(+), 72 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 952bb3b8352..4ef8440de62 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -24,7 +24,11 @@ async def auth_manager_from_config( hass: HomeAssistant, provider_configs: List[Dict[str, Any]], module_configs: List[Dict[str, Any]]) -> 'AuthManager': - """Initialize an auth manager from config.""" + """Initialize an auth manager from config. + + CORE_CONFIG_SCHEMA will make sure do duplicated auth providers or + mfa modules exist in configs. + """ store = auth_store.AuthStore(hass) if provider_configs: providers = await asyncio.gather( @@ -35,17 +39,7 @@ async def auth_manager_from_config( # So returned auth providers are in same order as config provider_hash = OrderedDict() # type: _ProviderDict for provider in providers: - if provider is None: - continue - key = (provider.type, provider.id) - - if key in provider_hash: - _LOGGER.error( - 'Found duplicate provider: %s. Please add unique IDs if you ' - 'want to have the same provider twice.', key) - continue - provider_hash[key] = provider if module_configs: @@ -57,15 +51,6 @@ async def auth_manager_from_config( # So returned auth modules are in same order as config module_hash = OrderedDict() # type: _MfaModuleDict for module in modules: - if module is None: - continue - - if module.id in module_hash: - _LOGGER.error( - 'Found duplicate multi-factor module: %s. Please add unique ' - 'IDs if you want to have the same module twice.', module.id) - continue - module_hash[module.id] = module manager = AuthManager(hass, store, provider_hash, module_hash) diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index a669f8bb5f0..603ca6ff3b1 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -11,6 +11,7 @@ from voluptuous.humanize import humanize_error from homeassistant import requirements, data_entry_flow from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.util.decorator import Registry MULTI_FACTOR_AUTH_MODULES = Registry() @@ -127,26 +128,23 @@ class SetupFlow(data_entry_flow.FlowHandler): async def auth_mfa_module_from_config( hass: HomeAssistant, config: Dict[str, Any]) \ - -> Optional[MultiFactorAuthModule]: + -> MultiFactorAuthModule: """Initialize an auth module from a config.""" module_name = config[CONF_TYPE] module = await _load_mfa_module(hass, module_name) - if module is None: - return None - try: config = module.CONFIG_SCHEMA(config) # type: ignore except vol.Invalid as err: _LOGGER.error('Invalid configuration for multi-factor module %s: %s', module_name, humanize_error(config, err)) - return None + raise return MULTI_FACTOR_AUTH_MODULES[module_name](hass, config) # type: ignore async def _load_mfa_module(hass: HomeAssistant, module_name: str) \ - -> Optional[types.ModuleType]: + -> types.ModuleType: """Load an mfa auth module.""" module_path = 'homeassistant.auth.mfa_modules.{}'.format(module_name) @@ -154,7 +152,8 @@ async def _load_mfa_module(hass: HomeAssistant, module_name: str) \ module = importlib.import_module(module_path) except ImportError as err: _LOGGER.error('Unable to load mfa module %s: %s', module_name, err) - return None + raise HomeAssistantError('Unable to load mfa module {}: {}'.format( + module_name, err)) if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'): return module @@ -170,7 +169,9 @@ async def _load_mfa_module(hass: HomeAssistant, module_name: str) \ hass, module_path, module.REQUIREMENTS) # type: ignore if not req_success: - return None + raise HomeAssistantError( + 'Unable to process requirements of mfa module {}'.format( + module_name)) processed.add(module_name) return module diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index d8ec04e9072..370391d57cd 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -10,6 +10,7 @@ from voluptuous.humanize import humanize_error from homeassistant import data_entry_flow, requirements from homeassistant.core import callback, HomeAssistant from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE +from homeassistant.exceptions import HomeAssistantError from homeassistant.util import dt as dt_util from homeassistant.util.decorator import Registry @@ -110,33 +111,31 @@ class AuthProvider: async def auth_provider_from_config( hass: HomeAssistant, store: AuthStore, - config: Dict[str, Any]) -> Optional[AuthProvider]: + config: Dict[str, Any]) -> AuthProvider: """Initialize an auth provider from a config.""" provider_name = config[CONF_TYPE] module = await load_auth_provider_module(hass, provider_name) - if module is None: - return None - try: config = module.CONFIG_SCHEMA(config) # type: ignore except vol.Invalid as err: _LOGGER.error('Invalid configuration for auth provider %s: %s', provider_name, humanize_error(config, err)) - return None + raise return AUTH_PROVIDERS[provider_name](hass, store, config) # type: ignore async def load_auth_provider_module( - hass: HomeAssistant, provider: str) -> Optional[types.ModuleType]: + hass: HomeAssistant, provider: str) -> types.ModuleType: """Load an auth provider.""" try: module = importlib.import_module( 'homeassistant.auth.providers.{}'.format(provider)) except ImportError as err: _LOGGER.error('Unable to load auth provider %s: %s', provider, err) - return None + raise HomeAssistantError('Unable to load auth provider {}: {}'.format( + provider, err)) if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'): return module @@ -154,7 +153,9 @@ async def load_auth_provider_module( hass, 'auth provider {}'.format(provider), reqs) if not req_success: - return None + raise HomeAssistantError( + 'Unable to process requirements of auth provider {}'.format( + provider)) processed.add(provider) return module diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index c10964e2da3..2051359c0ba 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -61,7 +61,6 @@ def from_config_dict(config: Dict[str, Any], config, hass, config_dir, enable_log, verbose, skip_pip, log_rotate_days, log_file, log_no_color) ) - return hass @@ -94,8 +93,13 @@ async def async_from_config_dict(config: Dict[str, Any], try: await conf_util.async_process_ha_core_config( hass, core_config, has_api_password, has_trusted_networks) - except vol.Invalid as ex: - conf_util.async_log_exception(ex, 'homeassistant', core_config, hass) + except vol.Invalid as config_err: + conf_util.async_log_exception( + config_err, 'homeassistant', core_config, hass) + return None + except HomeAssistantError: + _LOGGER.error("Home Assistant core failed to initialize. " + "Further initialization aborted") return None await hass.async_add_executor_job( @@ -130,7 +134,7 @@ async def async_from_config_dict(config: Dict[str, Any], res = await core_components.async_setup(hass, config) if not res: _LOGGER.error("Home Assistant core failed to initialize. " - "further initialization aborted") + "Further initialization aborted") return hass await persistent_notification.async_setup(hass, config) diff --git a/homeassistant/config.py b/homeassistant/config.py index a799094c94d..d742e62660b 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -8,7 +8,7 @@ import re import shutil # pylint: disable=unused-import from typing import ( # noqa: F401 - Any, Tuple, Optional, Dict, List, Union, Callable) + Any, Tuple, Optional, Dict, List, Union, Callable, Sequence, Set) from types import ModuleType import voluptuous as vol from voluptuous.humanize import humanize_error @@ -23,7 +23,7 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT, TEMP_CELSIUS, __version__, CONF_CUSTOMIZE, CONF_CUSTOMIZE_DOMAIN, CONF_CUSTOMIZE_GLOB, CONF_WHITELIST_EXTERNAL_DIRS, CONF_AUTH_PROVIDERS, CONF_AUTH_MFA_MODULES, - CONF_TYPE) + CONF_TYPE, CONF_ID) from homeassistant.core import callback, DOMAIN as CONF_CORE, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import get_component, get_platform @@ -128,6 +128,48 @@ some_password: welcome """ +def _no_duplicate_auth_provider(configs: Sequence[Dict[str, Any]]) \ + -> Sequence[Dict[str, Any]]: + """No duplicate auth provider config allowed in a list. + + Each type of auth provider can only have one config without optional id. + Unique id is required if same type of auth provider used multiple times. + """ + config_keys = set() # type: Set[Tuple[str, Optional[str]]] + for config in configs: + key = (config[CONF_TYPE], config.get(CONF_ID)) + if key in config_keys: + raise vol.Invalid( + 'Duplicate auth provider {} found. Please add unique IDs if ' + 'you want to have the same auth provider twice'.format( + config[CONF_TYPE] + )) + config_keys.add(key) + return configs + + +def _no_duplicate_auth_mfa_module(configs: Sequence[Dict[str, Any]]) \ + -> Sequence[Dict[str, Any]]: + """No duplicate auth mfa module item allowed in a list. + + Each type of mfa module can only have one config without optional id. + A global unique id is required if same type of mfa module used multiple + times. + Note: this is different than auth provider + """ + config_keys = set() # type: Set[str] + for config in configs: + key = config.get(CONF_ID, config[CONF_TYPE]) + if key in config_keys: + raise vol.Invalid( + 'Duplicate mfa module {} found. Please add unique IDs if ' + 'you want to have the same mfa module twice'.format( + config[CONF_TYPE] + )) + config_keys.add(key) + return configs + + PACKAGES_CONFIG_SCHEMA = vol.Schema({ cv.slug: vol.Schema( # Package names are slugs {cv.slug: vol.Any(dict, list, None)}) # Only slugs for component names @@ -166,10 +208,16 @@ CORE_CONFIG_SCHEMA = CUSTOMIZE_CONFIG_SCHEMA.extend({ CONF_TYPE: vol.NotIn(['insecure_example'], 'The insecure_example auth provider' ' is for testing only.') - })]), + })], + _no_duplicate_auth_provider), vol.Optional(CONF_AUTH_MFA_MODULES): vol.All(cv.ensure_list, - [auth_mfa_modules.MULTI_FACTOR_AUTH_MODULE_SCHEMA]), + [auth_mfa_modules.MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({ + CONF_TYPE: vol.NotIn(['insecure_example'], + 'The insecure_example mfa module' + ' is for testing only.') + })], + _no_duplicate_auth_mfa_module), }) diff --git a/tests/auth/providers/test_homeassistant.py b/tests/auth/providers/test_homeassistant.py index 935c5e50dd5..84beb8cdd3f 100644 --- a/tests/auth/providers/test_homeassistant.py +++ b/tests/auth/providers/test_homeassistant.py @@ -3,6 +3,7 @@ from unittest.mock import Mock import base64 import pytest +import voluptuous as vol from homeassistant import data_entry_flow from homeassistant.auth import auth_manager_from_config, auth_store @@ -111,11 +112,11 @@ async def test_saving_loading(data, hass): async def test_not_allow_set_id(): """Test we are not allowed to set an ID in config.""" hass = Mock() - provider = await auth_provider_from_config(hass, None, { - 'type': 'homeassistant', - 'id': 'invalid', - }) - assert provider is None + with pytest.raises(vol.Invalid): + await auth_provider_from_config(hass, None, { + 'type': 'homeassistant', + 'id': 'invalid', + }) async def test_new_users_populate_values(hass, data): diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index f724b40a71f..d9e7a50410f 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -3,6 +3,7 @@ from datetime import timedelta from unittest.mock import Mock, patch import pytest +import voluptuous as vol from homeassistant import auth, data_entry_flow from homeassistant.auth import ( @@ -21,33 +22,36 @@ def mock_hass(loop): return hass -async def test_auth_manager_from_config_validates_config_and_id(mock_hass): +async def test_auth_manager_from_config_validates_config(mock_hass): """Test get auth providers.""" + with pytest.raises(vol.Invalid): + manager = await auth.auth_manager_from_config(mock_hass, [{ + 'name': 'Test Name', + 'type': 'insecure_example', + 'users': [], + }, { + 'name': 'Invalid config because no users', + 'type': 'insecure_example', + 'id': 'invalid_config', + }], []) + manager = await auth.auth_manager_from_config(mock_hass, [{ 'name': 'Test Name', 'type': 'insecure_example', 'users': [], - }, { - 'name': 'Invalid config because no users', - 'type': 'insecure_example', - 'id': 'invalid_config', }, { 'name': 'Test Name 2', 'type': 'insecure_example', 'id': 'another', 'users': [], - }, { - 'name': 'Wrong because duplicate ID', - 'type': 'insecure_example', - 'id': 'another', - 'users': [], }], []) providers = [{ - 'name': provider.name, - 'id': provider.id, - 'type': provider.type, - } for provider in manager.auth_providers] + 'name': provider.name, + 'id': provider.id, + 'type': provider.type, + } for provider in manager.auth_providers] + assert providers == [{ 'name': 'Test Name', 'type': 'insecure_example', @@ -61,6 +65,26 @@ async def test_auth_manager_from_config_validates_config_and_id(mock_hass): async def test_auth_manager_from_config_auth_modules(mock_hass): """Test get auth modules.""" + with pytest.raises(vol.Invalid): + manager = await auth.auth_manager_from_config(mock_hass, [{ + 'name': 'Test Name', + 'type': 'insecure_example', + 'users': [], + }, { + 'name': 'Test Name 2', + 'type': 'insecure_example', + 'id': 'another', + 'users': [], + }], [{ + 'name': 'Module 1', + 'type': 'insecure_example', + 'data': [], + }, { + 'name': 'Invalid config because no data', + 'type': 'insecure_example', + 'id': 'another', + }]) + manager = await auth.auth_manager_from_config(mock_hass, [{ 'name': 'Test Name', 'type': 'insecure_example', @@ -79,13 +103,7 @@ async def test_auth_manager_from_config_auth_modules(mock_hass): 'type': 'insecure_example', 'id': 'another', 'data': [], - }, { - 'name': 'Duplicate ID', - 'type': 'insecure_example', - 'id': 'another', - 'data': [], }]) - providers = [{ 'name': provider.name, 'type': provider.type, diff --git a/tests/test_config.py b/tests/test_config.py index 3cfe67f70b1..e4a6798093f 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -895,9 +895,73 @@ async def test_disallowed_auth_provider_config(hass): 'name': 'Huis', CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, 'time_zone': 'GMT', - CONF_AUTH_PROVIDERS: [ - {'type': 'insecure_example'}, - ] + CONF_AUTH_PROVIDERS: [{ + 'type': 'insecure_example', + 'users': [{ + 'username': 'test-user', + 'password': 'test-pass', + 'name': 'Test Name' + }], + }] + } + with pytest.raises(Invalid): + await config_util.async_process_ha_core_config(hass, core_config) + + +async def test_disallowed_duplicated_auth_provider_config(hass): + """Test loading insecure example auth provider is disallowed.""" + core_config = { + 'latitude': 60, + 'longitude': 50, + 'elevation': 25, + 'name': 'Huis', + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, + 'time_zone': 'GMT', + CONF_AUTH_PROVIDERS: [{ + 'type': 'homeassistant', + }, { + 'type': 'homeassistant', + }] + } + with pytest.raises(Invalid): + await config_util.async_process_ha_core_config(hass, core_config) + + +async def test_disallowed_auth_mfa_module_config(hass): + """Test loading insecure example auth mfa module is disallowed.""" + core_config = { + 'latitude': 60, + 'longitude': 50, + 'elevation': 25, + 'name': 'Huis', + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, + 'time_zone': 'GMT', + CONF_AUTH_MFA_MODULES: [{ + 'type': 'insecure_example', + 'data': [{ + 'user_id': 'mock-user', + 'pin': 'test-pin' + }] + }] + } + with pytest.raises(Invalid): + await config_util.async_process_ha_core_config(hass, core_config) + + +async def test_disallowed_duplicated_auth_mfa_module_config(hass): + """Test loading insecure example auth mfa module is disallowed.""" + core_config = { + 'latitude': 60, + 'longitude': 50, + 'elevation': 25, + 'name': 'Huis', + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, + 'time_zone': 'GMT', + CONF_AUTH_MFA_MODULES: [{ + 'type': 'totp', + }, { + 'type': 'totp', + }] } with pytest.raises(Invalid): await config_util.async_process_ha_core_config(hass, core_config) From f891d0f5be80e711f46536bf0a763d581544bdf4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 28 Aug 2018 20:55:58 +0200 Subject: [PATCH 048/172] Update translations --- .../components/auth/.translations/lb.json | 16 ++++++++++ .../auth/.translations/zh-Hans.json | 16 ++++++++++ .../auth/.translations/zh-Hant.json | 16 ++++++++++ .../components/hangouts/.translations/lb.json | 31 +++++++++++++++++++ .../hangouts/.translations/zh-Hans.json | 29 +++++++++++++++++ .../hangouts/.translations/zh-Hant.json | 4 +-- .../.translations/zh-Hans.json | 1 + 7 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/auth/.translations/lb.json create mode 100644 homeassistant/components/auth/.translations/zh-Hans.json create mode 100644 homeassistant/components/auth/.translations/zh-Hant.json create mode 100644 homeassistant/components/hangouts/.translations/lb.json create mode 100644 homeassistant/components/hangouts/.translations/zh-Hans.json diff --git a/homeassistant/components/auth/.translations/lb.json b/homeassistant/components/auth/.translations/lb.json new file mode 100644 index 00000000000..f55ae4b97ba --- /dev/null +++ b/homeassistant/components/auth/.translations/lb.json @@ -0,0 +1,16 @@ +{ + "mfa_setup": { + "totp": { + "error": { + "invalid_code": "Ong\u00ebltege Login, prob\u00e9iert w.e.g. nach emol. Falls d\u00ebse Feeler Message \u00ebmmer er\u00ebm optr\u00ebtt dann iwwerpr\u00e9ift op d'Z\u00e4it vum Home Assistant System richteg ass." + }, + "step": { + "init": { + "description": "Fir d'Zwee-Faktor-Authentifikatioun m\u00ebttels engem Z\u00e4it bas\u00e9ierten eemolege Passwuert z'aktiv\u00e9ieren, scannt de QR Code mat enger Authentifikatioun's App.\nFalls dir keng hutt, recommand\u00e9iere mir entweder [Google Authenticator](https://support.google.com/accounts/answer/1066447) oder [Authy](https://authy.com/).\n\n{qr_code}\n\nNodeems de Code gescannt ass, gitt de sechs stellege Code vun der App a fir d'Konfiguratioun z'iwwerpr\u00e9iwen. Am Fall vu Problemer fir de QR Code ze scannen, gitt de folgende Code **`{code}`** a fir ee manuelle Setup.", + "title": "Zwee Faktor Authentifikatioun mat TOTP konfigur\u00e9ieren" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/zh-Hans.json b/homeassistant/components/auth/.translations/zh-Hans.json new file mode 100644 index 00000000000..c5b397a8e12 --- /dev/null +++ b/homeassistant/components/auth/.translations/zh-Hans.json @@ -0,0 +1,16 @@ +{ + "mfa_setup": { + "totp": { + "error": { + "invalid_code": "\u53e3\u4ee4\u65e0\u6548\uff0c\u8bf7\u91cd\u65b0\u8f93\u5165\u3002\u5982\u679c\u9519\u8bef\u53cd\u590d\u51fa\u73b0\uff0c\u8bf7\u786e\u4fdd Home Assistant \u7cfb\u7edf\u7684\u65f6\u95f4\u51c6\u786e\u65e0\u8bef\u3002" + }, + "step": { + "init": { + "description": "\u8981\u6fc0\u6d3b\u57fa\u4e8e\u65f6\u95f4\u52a8\u6001\u53e3\u4ee4\u7684\u53cc\u91cd\u8ba4\u8bc1\uff0c\u8bf7\u7528\u8eab\u4efd\u9a8c\u8bc1\u5e94\u7528\u626b\u63cf\u4ee5\u4e0b\u4e8c\u7ef4\u7801\u3002\u5982\u679c\u60a8\u8fd8\u6ca1\u6709\u8eab\u4efd\u9a8c\u8bc1\u5e94\u7528\uff0c\u63a8\u8350\u4f7f\u7528 [Google \u8eab\u4efd\u9a8c\u8bc1\u5668](https://support.google.com/accounts/answer/1066447) \u6216 [Authy](https://authy.com/)\u3002\n\n{qr_code}\n\n\u626b\u63cf\u4e8c\u7ef4\u7801\u4ee5\u540e\uff0c\u8f93\u5165\u5e94\u7528\u4e0a\u7684\u516d\u4f4d\u6570\u5b57\u53e3\u4ee4\u6765\u9a8c\u8bc1\u914d\u7f6e\u3002\u5982\u679c\u5728\u626b\u63cf\u4e8c\u7ef4\u7801\u65f6\u9047\u5230\u95ee\u9898\uff0c\u8bf7\u4f7f\u7528\u4ee3\u7801 **`{code}`** \u624b\u52a8\u914d\u7f6e\u3002", + "title": "\u7528\u65f6\u95f4\u52a8\u6001\u53e3\u4ee4\u8bbe\u7f6e\u53cc\u91cd\u8ba4\u8bc1" + } + }, + "title": "\u65f6\u95f4\u52a8\u6001\u53e3\u4ee4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/zh-Hant.json b/homeassistant/components/auth/.translations/zh-Hant.json new file mode 100644 index 00000000000..ef41ea87248 --- /dev/null +++ b/homeassistant/components/auth/.translations/zh-Hant.json @@ -0,0 +1,16 @@ +{ + "mfa_setup": { + "totp": { + "error": { + "invalid_code": "\u9a57\u8b49\u78bc\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002\u5047\u5982\u932f\u8aa4\u6301\u7e8c\u767c\u751f\uff0c\u8acb\u5148\u78ba\u5b9a\u60a8\u7684 Home Assistant \u7cfb\u7d71\u4e0a\u7684\u6642\u9593\u8a2d\u5b9a\u6b63\u78ba\u5f8c\uff0c\u518d\u8a66\u4e00\u6b21\u3002" + }, + "step": { + "init": { + "description": "\u6b32\u555f\u7528\u4e00\u6b21\u6027\u4e14\u5177\u6642\u6548\u6027\u7684\u5bc6\u78bc\u4e4b\u5169\u6b65\u9a5f\u9a57\u8b49\u529f\u80fd\uff0c\u8acb\u4f7f\u7528\u60a8\u7684\u9a57\u8b49 App \u6383\u7784\u4e0b\u65b9\u7684 QR code \u3002\u5018\u82e5\u60a8\u5c1a\u672a\u5b89\u88dd\u4efb\u4f55 App\uff0c\u63a8\u85a6\u60a8\u4f7f\u7528 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \u6216 [Authy](https://authy.com/)\u3002\n\n{qr_code}\n\n\u65bc\u6383\u63cf\u4e4b\u5f8c\uff0c\u8f38\u5165 App \u4e2d\u7684\u516d\u4f4d\u6578\u5b57\u9032\u884c\u8a2d\u5b9a\u9a57\u8b49\u3002\u5047\u5982\u6383\u63cf\u51fa\u73fe\u554f\u984c\uff0c\u8acb\u624b\u52d5\u8f38\u5165\u4ee5\u4e0b\u9a57\u8b49\u78bc **`{code}`**\u3002", + "title": "\u4f7f\u7528 TOTP \u8a2d\u5b9a\u5169\u6b65\u9a5f\u9a57\u8b49" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/lb.json b/homeassistant/components/hangouts/.translations/lb.json new file mode 100644 index 00000000000..426ab689626 --- /dev/null +++ b/homeassistant/components/hangouts/.translations/lb.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Google Hangouts ass scho konfigur\u00e9iert", + "unknown": "Onbekannten Fehler opgetrueden" + }, + "error": { + "invalid_2fa": "Ong\u00eblteg 2-Faktor Authentifikatioun, prob\u00e9iert w.e.g. nach emol.", + "invalid_2fa_method": "Ong\u00eblteg 2FA Methode (Iwwerpr\u00e9ift et um Telefon)", + "invalid_login": "Ong\u00ebltege Login, prob\u00e9iert w.e.g. nach emol." + }, + "step": { + "2fa": { + "data": { + "2fa": "2FA Pin" + }, + "description": "Eidel", + "title": "2-Faktor-Authentifikatioun" + }, + "user": { + "data": { + "email": "E-Mail Adress", + "password": "Passwuert" + }, + "description": "Eidel", + "title": "Google Hangouts Login" + } + }, + "title": "Google Hangouts" + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/zh-Hans.json b/homeassistant/components/hangouts/.translations/zh-Hans.json new file mode 100644 index 00000000000..bee6bf753db --- /dev/null +++ b/homeassistant/components/hangouts/.translations/zh-Hans.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Google Hangouts \u5df2\u914d\u7f6e\u5b8c\u6210", + "unknown": "\u53d1\u751f\u672a\u77e5\u9519\u8bef\u3002" + }, + "error": { + "invalid_2fa": "\u53cc\u91cd\u8ba4\u8bc1\u5931\u8d25\uff0c\u8bf7\u91cd\u8bd5\u3002", + "invalid_2fa_method": "\u65e0\u6548\u7684\u53cc\u91cd\u8ba4\u8bc1\u65b9\u6cd5\uff08\u7535\u8bdd\u9a8c\u8bc1\uff09\u3002", + "invalid_login": "\u767b\u9646\u5931\u8d25\uff0c\u8bf7\u518d\u8bd5\u4e00\u6b21\u3002" + }, + "step": { + "2fa": { + "data": { + "2fa": "2FA Pin" + }, + "title": "\u53cc\u91cd\u8ba4\u8bc1" + }, + "user": { + "data": { + "email": "\u7535\u5b50\u90ae\u4ef6\u5730\u5740", + "password": "\u5bc6\u7801" + }, + "title": "\u767b\u5f55 Google Hangouts" + } + }, + "title": "Google Hangouts" + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/zh-Hant.json b/homeassistant/components/hangouts/.translations/zh-Hant.json index 0920e0325d2..16234acb193 100644 --- a/homeassistant/components/hangouts/.translations/zh-Hant.json +++ b/homeassistant/components/hangouts/.translations/zh-Hant.json @@ -5,7 +5,7 @@ "unknown": "\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002" }, "error": { - "invalid_2fa": "\u5169\u968e\u6bb5\u9a57\u8b49\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", + "invalid_2fa": "\u5169\u6b65\u9a5f\u9a57\u8b49\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", "invalid_2fa_method": "\u8a8d\u8b49\u65b9\u5f0f\u7121\u6548\uff08\u65bc\u96fb\u8a71\u4e0a\u9a57\u8b49\uff09\u3002", "invalid_login": "\u767b\u5165\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002" }, @@ -15,7 +15,7 @@ "2fa": "\u8a8d\u8b49\u78bc" }, "description": "\u7a7a\u767d", - "title": "\u5169\u968e\u6bb5\u8a8d\u8b49" + "title": "\u5169\u6b65\u9a5f\u9a57\u8b49" }, "user": { "data": { diff --git a/homeassistant/components/homematicip_cloud/.translations/zh-Hans.json b/homeassistant/components/homematicip_cloud/.translations/zh-Hans.json index 38970e4a97c..930b649bceb 100644 --- a/homeassistant/components/homematicip_cloud/.translations/zh-Hans.json +++ b/homeassistant/components/homematicip_cloud/.translations/zh-Hans.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "\u63a5\u5165\u70b9\u5df2\u7ecf\u914d\u7f6e\u5b8c\u6210", "conection_aborted": "\u65e0\u6cd5\u8fde\u63a5\u5230 HMIP \u670d\u52a1\u5668", + "connection_aborted": "\u65e0\u6cd5\u8fde\u63a5\u5230 HMIP \u670d\u52a1\u5668", "unknown": "\u53d1\u751f\u672a\u77e5\u9519\u8bef\u3002" }, "error": { From 63614a477a40479de51fb0d253f8b51e2a8f9edb Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 29 Aug 2018 10:07:32 +0200 Subject: [PATCH 049/172] def device shouldnt call it self but self._device (#16255) --- homeassistant/components/media_player/plex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index 35906cf5023..46dacd98aad 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -531,7 +531,7 @@ class PlexClient(MediaPlayerDevice): @property def device(self): """Return the device, if any.""" - return self.device + return self._device @property def marked_unavailable(self): From 563588651c48fb39d2ff9c95beeaecc995c85ebd Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Wed, 29 Aug 2018 01:16:54 -0700 Subject: [PATCH 050/172] Tweak MFA login flow (#16254) * Tweak MFA login flow * Fix typo --- homeassistant/auth/mfa_modules/totp.py | 3 ++- homeassistant/auth/providers/__init__.py | 20 +++++++++++++------ .../auth/mfa_modules/test_insecure_example.py | 2 +- tests/auth/mfa_modules/test_totp.py | 2 +- tests/auth/test_init.py | 19 +++++------------- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index 48531863c1a..0914658a655 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -137,8 +137,9 @@ class TotpAuthModule(MultiFactorAuthModule): await self._async_load() # user_input has been validate in caller + # set INPUT_FIELD_CODE as vol.Required is not user friendly return await self.hass.async_add_executor_job( - self._validate_2fa, user_id, user_input[INPUT_FIELD_CODE]) + self._validate_2fa, user_id, user_input.get(INPUT_FIELD_CODE, '')) def _validate_2fa(self, user_id: str, code: str) -> bool: """Validate two factor authentication code.""" diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index 370391d57cd..3cb1c6b121e 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -224,19 +224,27 @@ class LoginFlow(data_entry_flow.FlowHandler): if user_input is not None: expires = self.created_at + SESSION_EXPIRATION if dt_util.utcnow() > expires: - errors['base'] = 'login_expired' - else: - result = await auth_module.async_validation( - self.user.id, user_input) # type: ignore - if not result: - errors['base'] = 'invalid_auth' + return self.async_abort( + reason='login_expired' + ) + + result = await auth_module.async_validation( + self.user.id, user_input) # type: ignore + if not result: + errors['base'] = 'invalid_code' if not errors: return await self.async_finish(self.user) + description_placeholders = { + 'mfa_module_name': auth_module.name, + 'mfa_module_id': auth_module.id + } # type: Dict[str, str] + return self.async_show_form( step_id='mfa', data_schema=auth_module.input_schema, + description_placeholders=description_placeholders, errors=errors, ) diff --git a/tests/auth/mfa_modules/test_insecure_example.py b/tests/auth/mfa_modules/test_insecure_example.py index e6f83762cd7..80109627140 100644 --- a/tests/auth/mfa_modules/test_insecure_example.py +++ b/tests/auth/mfa_modules/test_insecure_example.py @@ -119,7 +119,7 @@ async def test_login(hass): result = await hass.auth.login_flow.async_configure( result['flow_id'], {'pin': 'invalid-code'}) assert result['type'] == data_entry_flow.RESULT_TYPE_FORM - assert result['errors']['base'] == 'invalid_auth' + assert result['errors']['base'] == 'invalid_code' result = await hass.auth.login_flow.async_configure( result['flow_id'], {'pin': '123456'}) diff --git a/tests/auth/mfa_modules/test_totp.py b/tests/auth/mfa_modules/test_totp.py index 28e6c949bc4..6e3558ec549 100644 --- a/tests/auth/mfa_modules/test_totp.py +++ b/tests/auth/mfa_modules/test_totp.py @@ -121,7 +121,7 @@ async def test_login_flow_validates_mfa(hass): result['flow_id'], {'code': 'invalid-code'}) assert result['type'] == data_entry_flow.RESULT_TYPE_FORM assert result['step_id'] == 'mfa' - assert result['errors']['base'] == 'invalid_auth' + assert result['errors']['base'] == 'invalid_code' with patch('pyotp.TOTP.verify', return_value=True): result = await hass.auth.login_flow.async_configure( diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index d9e7a50410f..63b2b4408dd 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -428,10 +428,10 @@ async def test_login_with_auth_module(mock_hass): 'pin': 'invalid-pin', }) - # Invalid auth error + # Invalid code error assert step['type'] == data_entry_flow.RESULT_TYPE_FORM assert step['step_id'] == 'mfa' - assert step['errors'] == {'base': 'invalid_auth'} + assert step['errors'] == {'base': 'invalid_code'} step = await manager.login_flow.async_configure(step['flow_id'], { 'pin': 'test-pin', @@ -571,18 +571,9 @@ async def test_auth_module_expired_session(mock_hass): step = await manager.login_flow.async_configure(step['flow_id'], { 'pin': 'test-pin', }) - # Invalid auth due session timeout - assert step['type'] == data_entry_flow.RESULT_TYPE_FORM - assert step['step_id'] == 'mfa' - assert step['errors']['base'] == 'login_expired' - - # The second try will fail as well - step = await manager.login_flow.async_configure(step['flow_id'], { - 'pin': 'test-pin', - }) - assert step['type'] == data_entry_flow.RESULT_TYPE_FORM - assert step['step_id'] == 'mfa' - assert step['errors']['base'] == 'login_expired' + # login flow abort due session timeout + assert step['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert step['reason'] == 'login_expired' async def test_enable_mfa_for_user(hass, hass_storage): From 74c04294373ae14ce679e2fce18c3e4533ed092c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 29 Aug 2018 10:27:34 +0200 Subject: [PATCH 051/172] Bump frontend to 20180829.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index f0976c78224..0156a8b2cd6 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -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==20180827.0'] +REQUIREMENTS = ['home-assistant-frontend==20180829.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 85af517bb29..5868e6df5ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -444,7 +444,7 @@ hole==0.3.0 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180827.0 +home-assistant-frontend==20180829.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5dcf0550aba..94c35c30ddf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -84,7 +84,7 @@ hbmqtt==0.9.2 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180827.0 +home-assistant-frontend==20180829.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From e8801ee22f1e13554390cba3669da24055fab019 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 29 Aug 2018 10:28:34 +0200 Subject: [PATCH 052/172] Update translations --- .../components/auth/.translations/ko.json | 16 ++++++++++ .../components/auth/.translations/ru.json | 16 ++++++++++ .../components/hangouts/.translations/ko.json | 31 +++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 homeassistant/components/auth/.translations/ko.json create mode 100644 homeassistant/components/auth/.translations/ru.json create mode 100644 homeassistant/components/hangouts/.translations/ko.json diff --git a/homeassistant/components/auth/.translations/ko.json b/homeassistant/components/auth/.translations/ko.json new file mode 100644 index 00000000000..726fa6a6cd1 --- /dev/null +++ b/homeassistant/components/auth/.translations/ko.json @@ -0,0 +1,16 @@ +{ + "mfa_setup": { + "totp": { + "error": { + "invalid_code": "\uc798\ubabb\ub41c \ucf54\ub4dc \uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694. \uc774 \uc624\ub958\uac00 \uc9c0\uc18d\uc801\uc73c\ub85c \ubc1c\uc0dd\ud55c\ub2e4\uba74 Home Assistant \uc758 \uc2dc\uacc4\uac00 \uc815\ud655\ud55c\uc9c0 \ud655\uc778\ud574\ubcf4\uc138\uc694." + }, + "step": { + "init": { + "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574 \uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \ub098 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.", + "title": "TOTP \ub97c \uc0ac\uc6a9\ud558\uc5ec 2 \ub2e8\uacc4 \uc778\uc99d \uad6c\uc131" + } + }, + "title": "TOTP (\uc2dc\uac04 \uae30\ubc18 OTP)" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/ru.json b/homeassistant/components/auth/.translations/ru.json new file mode 100644 index 00000000000..b4b5b58f9fa --- /dev/null +++ b/homeassistant/components/auth/.translations/ru.json @@ -0,0 +1,16 @@ +{ + "mfa_setup": { + "totp": { + "error": { + "invalid_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430. \u0415\u0441\u043b\u0438 \u0432\u044b \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442\u0435 \u044d\u0442\u0443 \u043e\u0448\u0438\u0431\u043a\u0443, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0447\u0430\u0441\u044b \u0432 \u0432\u0430\u0448\u0435\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Home Assistant \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u044e\u0442 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0435 \u0432\u0440\u0435\u043c\u044f." + }, + "step": { + "init": { + "description": "\u0427\u0442\u043e\u0431\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0443\u044e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0445 \u043f\u0430\u0440\u043e\u043b\u0435\u0439, \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \u043d\u0430 \u0432\u0440\u0435\u043c\u0435\u043d\u0438, \u043e\u0442\u0441\u043a\u0430\u043d\u0438\u0440\u0443\u0439\u0442\u0435 QR-\u043a\u043e\u0434 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043f\u043e\u0434\u043b\u0438\u043d\u043d\u043e\u0441\u0442\u0438. \u0415\u0441\u043b\u0438 \u0443 \u0432\u0430\u0441 \u0435\u0433\u043e \u043d\u0435\u0442, \u043c\u044b \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043b\u0438\u0431\u043e [Google Authenticator] (https://support.google.com/accounts/answer/1066447), \u043b\u0438\u0431\u043e [Authy] (https://authy.com/). \n\n {qr_code} \n \n \u041f\u043e\u0441\u043b\u0435 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f QR-\u043a\u043e\u0434\u0430 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0448\u0435\u0441\u0442\u0438\u0437\u043d\u0430\u0447\u043d\u044b\u0439 \u043a\u043e\u0434 \u0438\u0437 \u0432\u0430\u0448\u0435\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u0415\u0441\u043b\u0438 \u0443 \u0432\u0430\u0441 \u0435\u0441\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441\u043e \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\u043c QR-\u043a\u043e\u0434\u0430, \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0440\u0443\u0447\u043d\u0443\u044e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0441 \u043a\u043e\u0434\u043e\u043c ** ` {code} ` **.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/ko.json b/homeassistant/components/hangouts/.translations/ko.json new file mode 100644 index 00000000000..aabf977a8cc --- /dev/null +++ b/homeassistant/components/hangouts/.translations/ko.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Google Hangouts \uc740 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", + "unknown": "\uc54c \uc218\uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "invalid_2fa": "2\ub2e8\uacc4 \uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574 \uc8fc\uc138\uc694.", + "invalid_2fa_method": "2\ub2e8\uacc4 \uc778\uc99d \ubc29\ubc95\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. (\uc804\ud654\uae30\uc5d0\uc11c \ud655\uc778)", + "invalid_login": "\uc798\ubabb\ub41c \ub85c\uadf8\uc778\uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." + }, + "step": { + "2fa": { + "data": { + "2fa": "2\ub2e8\uacc4 \uc778\uc99d PIN" + }, + "description": "\uc8c4\uc1a1\ud569\ub2c8\ub2e4. \uad00\ub828 \ub0b4\uc6a9\uc774 \uc544\uc9c1 \uc5c5\ub370\uc774\ud2b8 \ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \ucd94\ud6c4\uc5d0 \ubc18\uc601\ub420 \uc608\uc815\uc774\ub2c8 \uc870\uae08\ub9cc \uae30\ub2e4\ub824\uc8fc\uc138\uc694.", + "title": "2\ub2e8\uacc4 \uc778\uc99d" + }, + "user": { + "data": { + "email": "\uc774\uba54\uc77c \uc8fc\uc18c", + "password": "\ube44\ubc00\ubc88\ud638" + }, + "description": "\uc8c4\uc1a1\ud569\ub2c8\ub2e4. \uad00\ub828 \ub0b4\uc6a9\uc774 \uc544\uc9c1 \uc5c5\ub370\uc774\ud2b8 \ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \ucd94\ud6c4\uc5d0 \ubc18\uc601\ub420 \uc608\uc815\uc774\ub2c8 \uc870\uae08\ub9cc \uae30\ub2e4\ub824\uc8fc\uc138\uc694.", + "title": "Google Hangouts \ub85c\uadf8\uc778" + } + }, + "title": "Google Hangouts" + } +} \ No newline at end of file From 3df8840fee2fdc1eae5da96f59597fc5c49b9335 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 29 Aug 2018 12:20:05 +0200 Subject: [PATCH 053/172] Version bump to 0.78.0.dev0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index d72bde548d3..3bb468c1b1e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 77 +MINOR_VERSION = 78 PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) From 18ba50bc2de8d56a26c902cbafb0665afc8f3fab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Wed, 29 Aug 2018 12:56:15 +0200 Subject: [PATCH 054/172] Switchmate (#15535) * Switchmate * switchmate * swithcmate * switchmate * switchmate * fix comments * Update switchmate.py * change error log --- .coveragerc | 1 + homeassistant/components/switch/switchmate.py | 83 +++++++++++++++++++ requirements_all.txt | 1 + 3 files changed, 85 insertions(+) create mode 100644 homeassistant/components/switch/switchmate.py diff --git a/.coveragerc b/.coveragerc index 449883265f6..0c4a1f7d569 100644 --- a/.coveragerc +++ b/.coveragerc @@ -790,6 +790,7 @@ omit = homeassistant/components/switch/rest.py homeassistant/components/switch/rpi_rf.py homeassistant/components/switch/snmp.py + homeassistant/components/switch/switchmate.py homeassistant/components/switch/telnet.py homeassistant/components/switch/tplink.py homeassistant/components/switch/transmission.py diff --git a/homeassistant/components/switch/switchmate.py b/homeassistant/components/switch/switchmate.py new file mode 100644 index 00000000000..6ce4421ebc8 --- /dev/null +++ b/homeassistant/components/switch/switchmate.py @@ -0,0 +1,83 @@ +""" +Support for Switchmate. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.switchmate/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME, CONF_MAC +from homeassistant.exceptions import PlatformNotReady + +REQUIREMENTS = ['bluepy==1.1.4'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'Switchmate' +HANDLE = 0x2e +ON_KEY = b'\x00' +OFF_KEY = b'\x01' + +SCAN_INTERVAL = timedelta(minutes=30) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_MAC): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None) -> None: + """Perform the setup for Switchmate devices.""" + name = config.get(CONF_NAME) + mac_addr = config.get(CONF_MAC) + add_devices([Switchmate(mac_addr, name)], True) + + +class Switchmate(SwitchDevice): + """Representation of a Switchmate.""" + + def __init__(self, mac, name) -> None: + """Initialize the Switchmate.""" + # pylint: disable=import-error + import bluepy + self._state = False + self._name = name + self._mac = mac + try: + self._device = bluepy.btle.Peripheral(self._mac, + bluepy.btle.ADDR_TYPE_RANDOM) + except bluepy.btle.BTLEException: + _LOGGER.error("Failed to set up switchmate") + raise PlatformNotReady() + + @property + def unique_id(self) -> str: + """Return a unique, HASS-friendly identifier for this entity.""" + return self._mac.replace(':', '') + + @property + def name(self) -> str: + """Return the name of the switch.""" + return self._name + + def update(self) -> None: + """Synchronize state with switch.""" + self._state = self._device.readCharacteristic(HANDLE) == ON_KEY + + @property + def is_on(self) -> bool: + """Return true if it is on.""" + return self._state + + def turn_on(self, **kwargs) -> None: + """Turn the switch on.""" + self._device.writeCharacteristic(HANDLE, ON_KEY, True) + + def turn_off(self, **kwargs) -> None: + """Turn the switch off.""" + self._device.writeCharacteristic(HANDLE, OFF_KEY, True) diff --git a/requirements_all.txt b/requirements_all.txt index 5868e6df5ad..16c67016780 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -186,6 +186,7 @@ blinkstick==1.1.8 blockchain==1.4.4 # homeassistant.components.light.decora +# homeassistant.components.switch.switchmate # bluepy==1.1.4 # homeassistant.components.sensor.bme680 From aaa1ebeed54a06f739413d90f904f72b932de7e3 Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Wed, 29 Aug 2018 08:33:09 -0400 Subject: [PATCH 055/172] Add support for discrete states to MyQ cover (#16251) * Add discrete states and update dependency * Add translation dict --- homeassistant/components/cover/myq.py | 31 +++++++++++++++++++++++---- requirements_all.txt | 2 +- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/cover/myq.py b/homeassistant/components/cover/myq.py index bedc041fccc..6a17345188a 100644 --- a/homeassistant/components/cover/myq.py +++ b/homeassistant/components/cover/myq.py @@ -8,17 +8,25 @@ import logging import voluptuous as vol -from homeassistant.components.cover import CoverDevice +from homeassistant.components.cover import ( + CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN) from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, CONF_TYPE, STATE_CLOSED) + CONF_PASSWORD, CONF_TYPE, CONF_USERNAME, STATE_CLOSED, STATE_CLOSING, + STATE_OPENING) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pymyq==0.0.11'] +REQUIREMENTS = ['pymyq==0.0.15'] _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'myq' +MYQ_TO_HASS = { + 'closed': STATE_CLOSED, + 'closing': STATE_CLOSING, + 'opening': STATE_OPENING +} + NOTIFICATION_ID = 'myq_notification' NOTIFICATION_TITLE = 'MyQ Cover Setup' @@ -87,7 +95,17 @@ class MyQDevice(CoverDevice): @property def is_closed(self): """Return true if cover is closed, else False.""" - return self._status == STATE_CLOSED + return MYQ_TO_HASS[self._status] == STATE_CLOSED + + @property + def is_closing(self): + """Return if the cover is closing or not.""" + return MYQ_TO_HASS[self._status] == STATE_CLOSING + + @property + def is_opening(self): + """Return if the cover is opening or not.""" + return MYQ_TO_HASS[self._status] == STATE_OPENING def close_cover(self, **kwargs): """Issue close command to cover.""" @@ -97,6 +115,11 @@ class MyQDevice(CoverDevice): """Issue open command to cover.""" self.myq.open_device(self.device_id) + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_OPEN | SUPPORT_CLOSE + def update(self): """Update status of cover.""" self._status = self.myq.get_status(self.device_id) diff --git a/requirements_all.txt b/requirements_all.txt index 16c67016780..e31a2349dcf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -962,7 +962,7 @@ pymonoprice==0.3 pymusiccast==0.1.6 # homeassistant.components.cover.myq -pymyq==0.0.11 +pymyq==0.0.15 # homeassistant.components.mysensors pymysensors==0.17.0 From d46a1a266d19c30cd4d3e33e99751f77de517083 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 29 Aug 2018 14:32:47 +0100 Subject: [PATCH 056/172] bump version (#16262) --- homeassistant/components/upnp.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/upnp.py b/homeassistant/components/upnp.py index 5d7855f3959..2bf0572d498 100644 --- a/homeassistant/components/upnp.py +++ b/homeassistant/components/upnp.py @@ -15,7 +15,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers import discovery from homeassistant.util import get_local_ip -REQUIREMENTS = ['pyupnp-async==0.1.1.0'] +REQUIREMENTS = ['pyupnp-async==0.1.1.1'] DEPENDENCIES = ['http'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index e31a2349dcf..ba0da439cbd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1181,7 +1181,7 @@ pytrafikverket==0.1.5.8 pyunifi==2.13 # homeassistant.components.upnp -pyupnp-async==0.1.1.0 +pyupnp-async==0.1.1.1 # homeassistant.components.binary_sensor.uptimerobot pyuptimerobot==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 94c35c30ddf..2415d661e29 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -178,7 +178,7 @@ pytradfri[async]==5.5.1 pyunifi==2.13 # homeassistant.components.upnp -pyupnp-async==0.1.1.0 +pyupnp-async==0.1.1.1 # homeassistant.components.notify.html5 pywebpush==1.6.0 From 96cf6d59a3d2a8a8275d9c3ad0fc8d52ba1ae081 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 29 Aug 2018 15:43:01 +0200 Subject: [PATCH 057/172] Replace Authorization by Authentication (#16259) --- homeassistant/components/hangouts/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hangouts/strings.json b/homeassistant/components/hangouts/strings.json index 7e54586b810..dd421fee57a 100644 --- a/homeassistant/components/hangouts/strings.json +++ b/homeassistant/components/hangouts/strings.json @@ -6,7 +6,7 @@ }, "error": { "invalid_login": "Invalid Login, please try again.", - "invalid_2fa": "Invalid 2 Factor Authorization, please try again.", + "invalid_2fa": "Invalid 2 Factor Authentication, please try again.", "invalid_2fa_method": "Invalid 2FA Method (Verify on Phone)." }, "step": { @@ -23,7 +23,7 @@ "2fa": "2FA Pin" }, "description": "", - "title": "2-Factor-Authorization" + "title": "2-Factor-Authentication" } }, "title": "Google Hangouts" From 3934f7bf3a6a0b9f70ff719c5e5336063caa009c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 29 Aug 2018 15:46:09 +0200 Subject: [PATCH 058/172] Add device info to Chromecast (#16261) --- homeassistant/components/media_player/cast.py | 34 +++++++++++++++++-- tests/components/media_player/test_cast.py | 11 +++--- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index ae9589c7886..2954e427ed5 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -73,7 +73,8 @@ class ChromecastInfo: port = attr.ib(type=int) uuid = attr.ib(type=Optional[str], converter=attr.converters.optional(str), default=None) # always convert UUID to string if not None - model_name = attr.ib(type=str, default='') # needed for cast type + manufacturer = attr.ib(type=str, default='') + model_name = attr.ib(type=str, default='') friendly_name = attr.ib(type=Optional[str], default=None) @property @@ -111,6 +112,7 @@ def _fill_out_missing_chromecast_info(info: ChromecastInfo) -> ChromecastInfo: host=info.host, port=info.port, uuid=(info.uuid or http_device_status.uuid), friendly_name=(info.friendly_name or http_device_status.friendly_name), + manufacturer=(info.manufacturer or http_device_status.manufacturer), model_name=(info.model_name or http_device_status.model_name) ) @@ -148,7 +150,13 @@ def _setup_internal_discovery(hass: HomeAssistantType) -> None: def internal_callback(name): """Handle zeroconf discovery of a new chromecast.""" mdns = listener.services[name] - _discover_chromecast(hass, ChromecastInfo(*mdns)) + _discover_chromecast(hass, ChromecastInfo( + host=mdns[0], + port=mdns[1], + uuid=mdns[2], + model_name=mdns[3], + friendly_name=mdns[4], + )) _LOGGER.debug("Starting internal pychromecast discovery.") listener, browser = pychromecast.start_discovery(internal_callback) @@ -365,7 +373,10 @@ class CastDevice(MediaPlayerDevice): # pylint: disable=protected-access _LOGGER.debug("Connecting to cast device %s", cast_info) chromecast = await self.hass.async_add_job( - pychromecast._get_chromecast_from_host, attr.astuple(cast_info)) + pychromecast._get_chromecast_from_host, ( + cast_info.host, cast_info.port, cast_info.uuid, + cast_info.model_name, cast_info.friendly_name + )) self._chromecast = chromecast self._status_listener = CastStatusListener(self, chromecast) # Initialise connection status as connected because we can only @@ -494,6 +505,23 @@ class CastDevice(MediaPlayerDevice): """Return the name of the device.""" return self._cast_info.friendly_name + @property + def device_info(self): + """Return information about the device.""" + cast_info = self._cast_info + + if cast_info.model_name == "Google Cast Group": + return None + + return { + 'name': cast_info.friendly_name, + 'identifiers': { + (CAST_DOMAIN, cast_info.uuid.replace('-', '')) + }, + 'model': cast_info.model_name, + 'manufacturer': cast_info.manufacturer, + } + @property def state(self): """Return the state of the player.""" diff --git a/tests/components/media_player/test_cast.py b/tests/components/media_player/test_cast.py index 8fe285a59cd..7345fd0c158 100644 --- a/tests/components/media_player/test_cast.py +++ b/tests/components/media_player/test_cast.py @@ -77,7 +77,10 @@ async def async_setup_cast_internal_discovery(hass, config=None, def discover_chromecast(service_name: str, info: ChromecastInfo) -> None: """Discover a chromecast device.""" - listener.services[service_name] = attr.astuple(info) + listener.services[service_name] = ( + info.host, info.port, info.uuid, info.model_name, + info.friendly_name + ) discovery_callback(service_name) return discover_chromecast, add_entities @@ -152,8 +155,7 @@ async def test_internal_discovery_callback_only_generates_once(hass): discover_cast('the-service', info) await hass.async_block_till_done() discover = signal.mock_calls[0][1][0] - # attr's __eq__ somehow breaks here, use tuples instead - assert attr.astuple(discover) == attr.astuple(info) + assert discover == info signal.reset_mock() # discovering it a second time shouldn't @@ -183,8 +185,7 @@ async def test_internal_discovery_callback_fill_out(hass): # when called with incomplete info, it should use HTTP to get missing discover = signal.mock_calls[0][1][0] - # attr's __eq__ somehow breaks here, use tuples instead - assert attr.astuple(discover) == attr.astuple(full_info) + assert discover == full_info async def test_create_cast_device_without_uuid(hass): From 16a885824ded8c928cd7f45f63c988832c926ba9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 29 Aug 2018 16:27:08 +0200 Subject: [PATCH 059/172] Add device info for sonos (#16263) * Add device info for sonos * Sets --- homeassistant/components/media_player/sonos.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index c4309519e36..4fc6b8b0954 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -388,6 +388,18 @@ class SonosDevice(MediaPlayerDevice): """Return the name of the device.""" return self._name + @property + def device_info(self): + """Return information about the device.""" + return { + 'identifiers': { + (SONOS_DOMAIN, self._unique_id) + }, + 'name': self._name, + 'model': self._model.replace("Sonos ", ""), + 'manufacturer': 'Sonos', + } + @property @soco_coordinator def state(self): From 7751dd7535d98604ddc976533c13f849cdc6a0ee Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 29 Aug 2018 16:44:10 +0200 Subject: [PATCH 060/172] Add device info Nest (#16265) * Add device info Nest * Sets --- homeassistant/components/climate/nest.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index 321559f10ee..f81736b3a52 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -8,7 +8,8 @@ import logging import voluptuous as vol -from homeassistant.components.nest import DATA_NEST, SIGNAL_NEST_UPDATE +from homeassistant.components.nest import ( + DATA_NEST, SIGNAL_NEST_UPDATE, DOMAIN as NEST_DOMAIN) from homeassistant.components.climate import ( STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_ECO, ClimateDevice, PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, @@ -127,6 +128,18 @@ class NestThermostat(ClimateDevice): """Return unique ID for this device.""" return self.device.serial + @property + def device_info(self): + """Return information about the device.""" + return { + 'identifiers': { + (NEST_DOMAIN, self.device.device_id), + }, + 'name': self.device.name_long, + 'manufacturer': 'Nest Labs', + 'model': "Thermostat", + } + @property def name(self): """Return the name of the nest, if any.""" From 867d17b03d71662098d1de4dfcb490959d1fa0de Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 29 Aug 2018 17:04:04 +0200 Subject: [PATCH 061/172] Add Hue device info (#16267) * Add Hue device info * Set with tuples * Fix tests --- homeassistant/components/hue/__init__.py | 26 ++++++++++++++-- homeassistant/components/light/hue.py | 19 ++++++++++++ tests/components/hue/test_init.py | 38 +++++++++++++++++++++--- 3 files changed, 77 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index c04380e1303..38b521078f4 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -11,7 +11,8 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_FILENAME, CONF_HOST -from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers import ( + aiohttp_client, config_validation as cv, device_registry as dr) from .const import DOMAIN, API_NUPNP from .bridge import HueBridge @@ -132,7 +133,28 @@ async def async_setup_entry(hass, entry): bridge = HueBridge(hass, entry, allow_unreachable, allow_groups) hass.data[DOMAIN][host] = bridge - return await bridge.async_setup() + + if not await bridge.async_setup(): + return False + + config = bridge.api.config + device_registry = await dr.async_get_registry(hass) + device_registry.async_get_or_create( + config_entry=entry.entry_id, + connections={ + (dr.CONNECTION_NETWORK_MAC, config.mac) + }, + identifiers={ + (DOMAIN, config.bridgeid) + }, + manufacturer='Signify', + name=config.name, + # Not yet exposed as properties in aiohue + model=config.raw['modelid'], + sw_version=config.raw['swversion'], + ) + + return True async def async_unload_entry(hass, entry): diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 2a51423a7a8..6f6e0ed617e 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -285,6 +285,25 @@ class HueLight(Light): """Return the list of supported effects.""" return [EFFECT_COLORLOOP, EFFECT_RANDOM] + @property + def device_info(self): + """Return the device info.""" + if self.light.type in ('LightGroup', 'Room'): + return None + + return { + 'identifiers': { + (hue.DOMAIN, self.unique_id) + }, + 'name': self.name, + 'manufacturer': self.light.manufacturername, + # productname added in Hue Bridge API 1.24 + # (published 03/05/2018) + 'model': self.light.productname or self.light.modelid, + # Not yet exposed as properties in aiohue + 'sw_version': self.light.raw['swversion'], + } + async def async_turn_on(self, **kwargs): """Turn the specified or all lights on.""" command = {'on': True} diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index d12270cd908..1c4768746d5 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -1,5 +1,5 @@ """Test Hue setup process.""" -from unittest.mock import patch +from unittest.mock import Mock, patch from homeassistant.setup import async_setup_component from homeassistant.components import hue @@ -145,9 +145,21 @@ async def test_config_passed_to_config_entry(hass): 'host': '0.0.0.0', }) entry.add_to_hass(hass) - - with patch.object(hue, 'HueBridge') as mock_bridge: + mock_registry = Mock() + with patch.object(hue, 'HueBridge') as mock_bridge, \ + patch('homeassistant.helpers.device_registry.async_get_registry', + return_value=mock_coro(mock_registry)): mock_bridge.return_value.async_setup.return_value = mock_coro(True) + mock_bridge.return_value.api.config = Mock( + mac='mock-mac', + bridgeid='mock-bridgeid', + raw={ + 'modelid': 'mock-modelid', + 'swversion': 'mock-swversion', + } + ) + # Can't set name via kwargs + mock_bridge.return_value.api.config.name = 'mock-name' assert await async_setup_component(hass, hue.DOMAIN, { hue.DOMAIN: { hue.CONF_BRIDGES: { @@ -168,6 +180,21 @@ async def test_config_passed_to_config_entry(hass): assert p_allow_unreachable is True assert p_allow_groups is False + assert len(mock_registry.mock_calls) == 1 + assert mock_registry.mock_calls[0][2] == { + 'config_entry': entry.entry_id, + 'connections': { + ('mac', 'mock-mac') + }, + 'identifiers': { + ('hue', 'mock-bridgeid') + }, + 'manufacturer': 'Signify', + 'name': 'mock-name', + 'model': 'mock-modelid', + 'sw_version': 'mock-swversion' + } + async def test_unload_entry(hass): """Test being able to unload an entry.""" @@ -176,8 +203,11 @@ async def test_unload_entry(hass): }) entry.add_to_hass(hass) - with patch.object(hue, 'HueBridge') as mock_bridge: + with patch.object(hue, 'HueBridge') as mock_bridge, \ + patch('homeassistant.helpers.device_registry.async_get_registry', + return_value=mock_coro(Mock())): mock_bridge.return_value.async_setup.return_value = mock_coro(True) + mock_bridge.return_value.api.config = Mock() assert await async_setup_component(hass, hue.DOMAIN, {}) is True assert len(mock_bridge.return_value.mock_calls) == 1 From 5681fa8f07a6db5d77d343429d5ec4f73cf9f139 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Wed, 29 Aug 2018 12:00:40 -0700 Subject: [PATCH 062/172] Nest Thermostat has software version (#16275) --- homeassistant/components/climate/nest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index f81736b3a52..bc63512fcf3 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -138,6 +138,7 @@ class NestThermostat(ClimateDevice): 'name': self.device.name_long, 'manufacturer': 'Nest Labs', 'model': "Thermostat", + 'sw_version': self.device.software_version, } @property From 99d48795b9be6be2dc1bbfd92e375285cba508e6 Mon Sep 17 00:00:00 2001 From: Pavel Pletenev Date: Wed, 29 Aug 2018 21:13:01 +0200 Subject: [PATCH 063/172] Add support for Habitica (#15744) * Added support for Habitica Second refactoring Moved all config to component. Sensors are autodiscovered. Signed-off-by: delphi * Apply requested changes Signed-off-by: delphi * Made event fire async. Made `sensors` config implicit and opt-out-style. Signed-off-by: delphi * Removed unneeded check and await. Signed-off-by: delphi * Moved into separate component package and added service.yaml Signed-off-by: delphi * Fix coveralls Signed-off-by: delphi --- .coveragerc | 3 + homeassistant/components/habitica/__init__.py | 158 ++++++++++++++++++ .../components/habitica/services.yaml | 15 ++ homeassistant/components/sensor/habitica.py | 96 +++++++++++ requirements_all.txt | 3 + 5 files changed, 275 insertions(+) create mode 100644 homeassistant/components/habitica/__init__.py create mode 100644 homeassistant/components/habitica/services.yaml create mode 100644 homeassistant/components/sensor/habitica.py diff --git a/.coveragerc b/.coveragerc index 0c4a1f7d569..39c31e4e40b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -116,6 +116,9 @@ omit = homeassistant/components/google.py homeassistant/components/*/google.py + homeassistant/components/habitica/* + homeassistant/components/*/habitica.py + homeassistant/components/hangouts/__init__.py homeassistant/components/hangouts/const.py homeassistant/components/hangouts/hangouts_bot.py diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py new file mode 100644 index 00000000000..44b9e392157 --- /dev/null +++ b/homeassistant/components/habitica/__init__.py @@ -0,0 +1,158 @@ +""" +The Habitica API component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/habitica/ +""" + +import logging +from collections import namedtuple + +import voluptuous as vol +from homeassistant.const import \ + CONF_NAME, CONF_URL, CONF_SENSORS, CONF_PATH, CONF_API_KEY +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers import \ + config_validation as cv, discovery + +REQUIREMENTS = ['habitipy==0.2.0'] +_LOGGER = logging.getLogger(__name__) +DOMAIN = "habitica" + +CONF_API_USER = "api_user" + +ST = SensorType = namedtuple('SensorType', [ + "name", "icon", "unit", "path" +]) + +SENSORS_TYPES = { + 'name': ST('Name', None, '', ["profile", "name"]), + 'hp': ST('HP', 'mdi:heart', 'HP', ["stats", "hp"]), + 'maxHealth': ST('max HP', 'mdi:heart', 'HP', ["stats", "maxHealth"]), + 'mp': ST('Mana', 'mdi:auto-fix', 'MP', ["stats", "mp"]), + 'maxMP': ST('max Mana', 'mdi:auto-fix', 'MP', ["stats", "maxMP"]), + 'exp': ST('EXP', 'mdi:star', 'EXP', ["stats", "exp"]), + 'toNextLevel': ST( + 'Next Lvl', 'mdi:star', 'EXP', ["stats", "toNextLevel"]), + 'lvl': ST( + 'Lvl', 'mdi:arrow-up-bold-circle-outline', 'Lvl', ["stats", "lvl"]), + 'gp': ST('Gold', 'mdi:coin', 'Gold', ["stats", "gp"]), + 'class': ST('Class', 'mdi:sword', '', ["stats", "class"]) +} + +INSTANCE_SCHEMA = vol.Schema({ + vol.Optional(CONF_URL, default='https://habitica.com'): cv.url, + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_API_USER): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_SENSORS, default=list(SENSORS_TYPES)): + vol.All( + cv.ensure_list, + vol.Unique(), + [vol.In(list(SENSORS_TYPES))]) +}) + +has_unique_values = vol.Schema(vol.Unique()) # pylint: disable=invalid-name +# because we want a handy alias + + +def has_all_unique_users(value): + """Validate that all `api_user`s are unique.""" + api_users = [user[CONF_API_USER] for user in value] + has_unique_values(api_users) + return value + + +def has_all_unique_users_names(value): + """Validate that all user's names are unique and set if any is set.""" + names = [user.get(CONF_NAME) for user in value] + if None in names and any(name is not None for name in names): + raise vol.Invalid( + 'user names of all users must be set if any is set') + if not all(name is None for name in names): + has_unique_values(names) + return value + + +INSTANCE_LIST_SCHEMA = vol.All( + cv.ensure_list, + has_all_unique_users, + has_all_unique_users_names, + [INSTANCE_SCHEMA]) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: INSTANCE_LIST_SCHEMA +}, extra=vol.ALLOW_EXTRA) + +SERVICE_API_CALL = 'api_call' +ATTR_NAME = CONF_NAME +ATTR_PATH = CONF_PATH +ATTR_ARGS = "args" +EVENT_API_CALL_SUCCESS = "{0}_{1}_{2}".format( + DOMAIN, SERVICE_API_CALL, "success") + +SERVICE_API_CALL_SCHEMA = vol.Schema({ + vol.Required(ATTR_NAME): str, + vol.Required(ATTR_PATH): vol.All(cv.ensure_list, [str]), + vol.Optional(ATTR_ARGS): dict +}) + + +async def async_setup(hass, config): + """Set up the habitica service.""" + conf = config[DOMAIN] + data = hass.data[DOMAIN] = {} + websession = async_get_clientsession(hass) + from habitipy.aio import HabitipyAsync + + class HAHabitipyAsync(HabitipyAsync): + """Closure API class to hold session.""" + + def __call__(self, **kwargs): + return super().__call__(websession, **kwargs) + + for instance in conf: + url = instance[CONF_URL] + username = instance[CONF_API_USER] + password = instance[CONF_API_KEY] + name = instance.get(CONF_NAME) + config_dict = {"url": url, "login": username, "password": password} + api = HAHabitipyAsync(config_dict) + user = await api.user.get() + if name is None: + name = user['profile']['name'] + data[name] = api + if CONF_SENSORS in instance: + hass.async_create_task( + discovery.async_load_platform( + hass, "sensor", DOMAIN, + {"name": name, "sensors": instance[CONF_SENSORS]}, + config)) + + async def handle_api_call(call): + name = call.data[ATTR_NAME] + path = call.data[ATTR_PATH] + api = hass.data[DOMAIN].get(name) + if api is None: + _LOGGER.error( + "API_CALL: User '%s' not configured", name) + return + try: + for element in path: + api = api[element] + except KeyError: + _LOGGER.error( + "API_CALL: Path %s is invalid" + " for api on '{%s}' element", path, element) + return + kwargs = call.data.get(ATTR_ARGS, {}) + data = await api(**kwargs) + hass.bus.async_fire(EVENT_API_CALL_SUCCESS, { + "name": name, "path": path, "data": data + }) + + hass.services.async_register( + DOMAIN, SERVICE_API_CALL, + handle_api_call, + schema=SERVICE_API_CALL_SCHEMA) + return True diff --git a/homeassistant/components/habitica/services.yaml b/homeassistant/components/habitica/services.yaml new file mode 100644 index 00000000000..a063b1577f5 --- /dev/null +++ b/homeassistant/components/habitica/services.yaml @@ -0,0 +1,15 @@ +# Describes the format for Habitica service + +--- +api_call: + description: Call Habitica api + fields: + name: + description: Habitica's username to call for + example: 'xxxNotAValidNickxxx' + path: + description: "Items from API URL in form of an array with method attached at the end. Consult https://habitica.com/apidoc/. Example uses https://habitica.com/apidoc/#api-Task-CreateUserTasks" + example: '["tasks", "user", "post"]' + args: + description: Any additional json or url parameter arguments. See apidoc mentioned for path. Example uses same api endpoint + example: '{"text": "Use API from Home Assistant", "type": "todo"}' diff --git a/homeassistant/components/sensor/habitica.py b/homeassistant/components/sensor/habitica.py new file mode 100644 index 00000000000..d2f13eb30e6 --- /dev/null +++ b/homeassistant/components/sensor/habitica.py @@ -0,0 +1,96 @@ +""" +The Habitica sensor. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.habitica/ +""" + +import logging +from datetime import timedelta + +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle +from homeassistant.components import habitica + +_LOGGER = logging.getLogger(__name__) +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) + + +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): + """Set up the habitica platform.""" + if discovery_info is None: + return + + name = discovery_info[habitica.CONF_NAME] + sensors = discovery_info[habitica.CONF_SENSORS] + sensor_data = HabitipyData(hass.data[habitica.DOMAIN][name]) + await sensor_data.update() + async_add_devices([ + HabitipySensor(name, sensor, sensor_data) + for sensor in sensors + ], True) + + +class HabitipyData: + """Habitica API user data cache.""" + + def __init__(self, api): + """ + Habitica API user data cache. + + api - HAHabitipyAsync object + """ + self.api = api + self.data = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def update(self): + """Get a new fix from Habitica servers.""" + self.data = await self.api.user.get() + + +class HabitipySensor(Entity): + """A generic Habitica sensor.""" + + def __init__(self, name, sensor_name, updater): + """ + Init a generic Habitica sensor. + + name - Habitica platform name + sensor_name - one of the names from ALL_SENSOR_TYPES + """ + self._name = name + self._sensor_name = sensor_name + self._sensor_type = habitica.SENSORS_TYPES[sensor_name] + self._state = None + self._updater = updater + + async def async_update(self): + """Update Condition and Forecast.""" + await self._updater.update() + data = self._updater.data + for element in self._sensor_type.path: + data = data[element] + self._state = data + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return self._sensor_type.icon + + @property + def name(self): + """Return the name of the sensor.""" + return "{0}_{1}_{2}".format( + habitica.DOMAIN, self._name, self._sensor_name) + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._sensor_type.unit diff --git a/requirements_all.txt b/requirements_all.txt index ba0da439cbd..ef89fb096da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -420,6 +420,9 @@ ha-ffmpeg==1.9 # homeassistant.components.media_player.philips_js ha-philipsjs==0.0.5 +# homeassistant.components.habitica +habitipy==0.2.0 + # homeassistant.components.hangouts hangups==0.4.5 From 25ee8e551cab9855c1bd1c2cdc8e1b0c4d1a2192 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Wed, 29 Aug 2018 22:29:34 +0200 Subject: [PATCH 064/172] Fix data_key override by parent class (#16278) --- homeassistant/components/binary_sensor/xiaomi_aqara.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/binary_sensor/xiaomi_aqara.py b/homeassistant/components/binary_sensor/xiaomi_aqara.py index c42090e3b7a..730b662b90b 100644 --- a/homeassistant/components/binary_sensor/xiaomi_aqara.py +++ b/homeassistant/components/binary_sensor/xiaomi_aqara.py @@ -374,11 +374,11 @@ class XiaomiCube(XiaomiBinarySensor): self._last_action = None self._state = False if 'proto' not in device or int(device['proto'][0:1]) == 1: - self._data_key = 'status' + data_key = 'status' else: - self._data_key = 'cube_status' + data_key = 'cube_status' XiaomiBinarySensor.__init__(self, device, 'Cube', xiaomi_hub, - None, None) + data_key, None) @property def device_state_attributes(self): From 87df1027728a403e68895d66cc91e2ee152b315b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 29 Aug 2018 22:59:55 +0200 Subject: [PATCH 065/172] Bump frontend to 20180829.1 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 0156a8b2cd6..da3d225bba0 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -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==20180829.0'] +REQUIREMENTS = ['home-assistant-frontend==20180829.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index ef89fb096da..c788a242834 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -448,7 +448,7 @@ hole==0.3.0 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180829.0 +home-assistant-frontend==20180829.1 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2415d661e29..8e10876cc06 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -84,7 +84,7 @@ hbmqtt==0.9.2 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180829.0 +home-assistant-frontend==20180829.1 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 88f72a654a701b407f4c99275549570e9e56b6d3 Mon Sep 17 00:00:00 2001 From: Conrad Juhl Andersen Date: Wed, 29 Aug 2018 23:17:18 +0200 Subject: [PATCH 066/172] Fix error when vacuum is idling (#16282) --- homeassistant/components/vacuum/xiaomi_miio.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py index a6d8fccdee0..41842459c8a 100644 --- a/homeassistant/components/vacuum/xiaomi_miio.py +++ b/homeassistant/components/vacuum/xiaomi_miio.py @@ -87,6 +87,7 @@ SUPPORT_XIAOMI = SUPPORT_STATE | SUPPORT_PAUSE | \ STATE_CODE_TO_STATE = { + 2: STATE_IDLE, 3: STATE_IDLE, 5: STATE_CLEANING, 6: STATE_RETURNING, From 645c3a67d818f6103bd125b6886d6c1a740a9d53 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 29 Aug 2018 23:18:20 +0200 Subject: [PATCH 067/172] Fix so that entities are properly unloaded with config entry (#16281) --- .../components/binary_sensor/deconz.py | 5 +++ homeassistant/components/deconz/__init__.py | 19 +++++++-- homeassistant/components/light/deconz.py | 5 +++ homeassistant/components/scene/deconz.py | 4 ++ homeassistant/components/sensor/deconz.py | 40 ++++++++++++------- homeassistant/components/switch/deconz.py | 5 +++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 62 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py index 1fb62124407..d2ca9e7c5e8 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -54,6 +54,11 @@ class DeconzBinarySensor(BinarySensorDevice): self._sensor.register_async_callback(self.async_update_callback) self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._sensor.deconz_id + async def async_will_remove_from_hass(self) -> None: + """Disconnect sensor object when removed.""" + self._sensor.remove_callback(self.async_update_callback) + self._sensor = None + @callback def async_update_callback(self, reason): """Update the sensor's state. diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index a4edc009ea1..e9f797d95f9 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -24,7 +24,7 @@ from .const import ( CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER) -REQUIREMENTS = ['pydeconz==44'] +REQUIREMENTS = ['pydeconz==45'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -179,15 +179,22 @@ async def async_unload_entry(hass, config_entry): deconz = hass.data.pop(DOMAIN) hass.services.async_remove(DOMAIN, SERVICE_DECONZ) deconz.close() - for component in ['binary_sensor', 'light', 'scene', 'sensor']: + + for component in ['binary_sensor', 'light', 'scene', 'sensor', 'switch']: await hass.config_entries.async_forward_entry_unload( config_entry, component) + dispatchers = hass.data[DATA_DECONZ_UNSUB] for unsub_dispatcher in dispatchers: unsub_dispatcher() hass.data[DATA_DECONZ_UNSUB] = [] - hass.data[DATA_DECONZ_EVENT] = [] + + for event in hass.data[DATA_DECONZ_EVENT]: + event.async_will_remove_from_hass() + hass.data[DATA_DECONZ_EVENT].remove(event) + hass.data[DATA_DECONZ_ID] = [] + return True @@ -206,6 +213,12 @@ class DeconzEvent: self._event = 'deconz_{}'.format(CONF_EVENT) self._id = slugify(self._device.name) + @callback + def async_will_remove_from_hass(self) -> None: + """Disconnect event object when removed.""" + self._device.remove_callback(self.async_update_callback) + self._device = None + @callback def async_update_callback(self, reason): """Fire the event if reason is that state is updated.""" diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index 412cf8693e5..ff3fe609924 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -82,6 +82,11 @@ class DeconzLight(Light): self._light.register_async_callback(self.async_update_callback) self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._light.deconz_id + async def async_will_remove_from_hass(self) -> None: + """Disconnect light object when removed.""" + self._light.remove_callback(self.async_update_callback) + self._light = None + @callback def async_update_callback(self, reason): """Update the light's state.""" diff --git a/homeassistant/components/scene/deconz.py b/homeassistant/components/scene/deconz.py index dde78dadc49..5af8f657206 100644 --- a/homeassistant/components/scene/deconz.py +++ b/homeassistant/components/scene/deconz.py @@ -38,6 +38,10 @@ class DeconzScene(Scene): """Subscribe to sensors events.""" self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._scene.deconz_id + async def async_will_remove_from_hass(self) -> None: + """Disconnect scene object when removed.""" + self._scene = None + async def async_activate(self): """Activate the scene.""" await self._scene.async_set_state({}) diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py index 8cb3915dc46..37fab727299 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -64,6 +64,11 @@ class DeconzSensor(Entity): self._sensor.register_async_callback(self.async_update_callback) self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._sensor.deconz_id + async def async_will_remove_from_hass(self) -> None: + """Disconnect sensor object when removed.""" + self._sensor.remove_callback(self.async_update_callback) + self._sensor = None + @callback def async_update_callback(self, reason): """Update the sensor's state. @@ -155,16 +160,21 @@ class DeconzSensor(Entity): class DeconzBattery(Entity): """Battery class for when a device is only represented as an event.""" - def __init__(self, device): + def __init__(self, sensor): """Register dispatcher callback for update of battery state.""" - self._device = device - self._name = '{} {}'.format(self._device.name, 'Battery Level') + self._sensor = sensor + self._name = '{} {}'.format(self._sensor.name, 'Battery Level') self._unit_of_measurement = "%" async def async_added_to_hass(self): """Subscribe to sensors events.""" - self._device.register_async_callback(self.async_update_callback) - self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._device.deconz_id + self._sensor.register_async_callback(self.async_update_callback) + self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._sensor.deconz_id + + async def async_will_remove_from_hass(self) -> None: + """Disconnect sensor object when removed.""" + self._sensor.remove_callback(self.async_update_callback) + self._sensor = None @callback def async_update_callback(self, reason): @@ -175,7 +185,7 @@ class DeconzBattery(Entity): @property def state(self): """Return the state of the battery.""" - return self._device.battery + return self._sensor.battery @property def name(self): @@ -185,7 +195,7 @@ class DeconzBattery(Entity): @property def unique_id(self): """Return a unique identifier for the device.""" - return self._device.uniqueid + return self._sensor.uniqueid @property def device_class(self): @@ -206,22 +216,22 @@ class DeconzBattery(Entity): def device_state_attributes(self): """Return the state attributes of the battery.""" attr = { - ATTR_EVENT_ID: slugify(self._device.name), + ATTR_EVENT_ID: slugify(self._sensor.name), } return attr @property def device_info(self): """Return a device description for device registry.""" - if (self._device.uniqueid is None or - self._device.uniqueid.count(':') != 7): + if (self._sensor.uniqueid is None or + self._sensor.uniqueid.count(':') != 7): return None - serial = self._device.uniqueid.split('-', 1)[0] + serial = self._sensor.uniqueid.split('-', 1)[0] return { 'connections': {(CONNECTION_ZIGBEE, serial)}, 'identifiers': {(DECONZ_DOMAIN, serial)}, - 'manufacturer': self._device.manufacturer, - 'model': self._device.modelid, - 'name': self._device.name, - 'sw_version': self._device.swversion, + 'manufacturer': self._sensor.manufacturer, + 'model': self._sensor.modelid, + 'name': self._sensor.name, + 'sw_version': self._sensor.swversion, } diff --git a/homeassistant/components/switch/deconz.py b/homeassistant/components/switch/deconz.py index 35dbc3ef782..bd8167d89a0 100644 --- a/homeassistant/components/switch/deconz.py +++ b/homeassistant/components/switch/deconz.py @@ -55,6 +55,11 @@ class DeconzSwitch(SwitchDevice): self._switch.register_async_callback(self.async_update_callback) self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._switch.deconz_id + async def async_will_remove_from_hass(self) -> None: + """Disconnect switch object when removed.""" + self._switch.remove_callback(self.async_update_callback) + self._switch = None + @callback def async_update_callback(self, reason): """Update the switch's state.""" diff --git a/requirements_all.txt b/requirements_all.txt index c788a242834..54d902f4260 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -820,7 +820,7 @@ pycsspeechtts==1.0.2 pydaikin==0.4 # homeassistant.components.deconz -pydeconz==44 +pydeconz==45 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8e10876cc06..e0d1596db3f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -139,7 +139,7 @@ py-canary==0.5.0 pyblackbird==0.5 # homeassistant.components.deconz -pydeconz==44 +pydeconz==45 # homeassistant.components.zwave pydispatcher==2.0.5 From 54c3f4f00169c6b9f6b0a4c0f4607a9ffd29f341 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Wed, 29 Aug 2018 14:59:48 -0700 Subject: [PATCH 068/172] Fix spelling mistake in recorder migration [ci skip] --- homeassistant/components/recorder/migration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 207f2f53a7f..7b257e223db 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -169,7 +169,7 @@ def _add_columns(engine, table_name, columns_def): if 'duplicate' not in str(err).lower(): raise - _LOGGER.warning('Column %s already exists on %s, continueing', + _LOGGER.warning('Column %s already exists on %s, continuing', column_def.split(' ')[1], table_name) From f20a3313b09129b725fec0ac4064bafe940db233 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Thu, 30 Aug 2018 21:58:23 +1000 Subject: [PATCH 069/172] Geo Location component (#15953) * initial working version of a geo location component and georss platform * ensure that custom attributes don't override built-in ones * bugfixes and tests * fixing tests because of introduction of new component using same fixture * improving test cases * removing potentially unavailable attribute from debug message output * completing test suite * cleaning up debug messages; sorting entries in group view by distance * ability to define the desired state attribute and corresponding unit of measurement; sort devices in group by configured state; find centroid for map if event is defined by polygon; updated tests * sort entries in group; code clean-ups * fixing indentation * added requirements of new component and platform * fixed various lint issues * fixed more lint issues * introducing demo geo location platform; refactored geo location component and geo rss platform to fit * removing geo rss events platform; added unit tests for geo location platform and demo platform * reverting change in debug message for feedreader to avoid confusion with new geo location component * updated requirements after removing georss platform * removed unused imports * fixing a lint issue and a test case * simplifying component code; moving code into demo platform; fixing tests * removed grouping from demo platform; small refactorings * automating the entity id generation (the use of an entity namespace achieves the same thing) * undoing changes made for the georss platform * simplified test cases * small tweaks to test case * rounding all state attribute values * fixing lint; removing distance from state attributes * fixed test * renamed add_devices to add_entities; tweaked test to gain more control over the timed update in the demo platform * reusing utcnow variable instead of patched method * fixed test by avoiding to make assumptions about order of list of entity ids * adding test for the geo location event class --- .../components/geo_location/__init__.py | 68 +++++++++ homeassistant/components/geo_location/demo.py | 132 ++++++++++++++++++ tests/components/geo_location/__init__.py | 1 + tests/components/geo_location/test_demo.py | 63 +++++++++ tests/components/geo_location/test_init.py | 20 +++ 5 files changed, 284 insertions(+) create mode 100644 homeassistant/components/geo_location/__init__.py create mode 100644 homeassistant/components/geo_location/demo.py create mode 100644 tests/components/geo_location/__init__.py create mode 100644 tests/components/geo_location/test_demo.py create mode 100644 tests/components/geo_location/test_init.py diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py new file mode 100644 index 00000000000..67ed9520fa4 --- /dev/null +++ b/homeassistant/components/geo_location/__init__.py @@ -0,0 +1,68 @@ +""" +Geo Location component. + +This component covers platforms that deal with external events that contain +a geo location related to the installed HA instance. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/geo_location/ +""" +import logging +from datetime import timedelta +from typing import Optional + +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent + +_LOGGER = logging.getLogger(__name__) + +ATTR_DISTANCE = 'distance' +DOMAIN = 'geo_location' +ENTITY_ID_FORMAT = DOMAIN + '.{}' +GROUP_NAME_ALL_EVENTS = 'All Geo Location Events' +SCAN_INTERVAL = timedelta(seconds=60) + + +async def async_setup(hass, config): + """Set up this component.""" + component = EntityComponent( + _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_EVENTS) + await component.async_setup(config) + return True + + +class GeoLocationEvent(Entity): + """This represents an external event with an associated geo location.""" + + @property + def state(self): + """Return the state of the sensor.""" + if self.distance is not None: + return round(self.distance, 1) + return None + + @property + def distance(self) -> Optional[float]: + """Return distance value of this external event.""" + return None + + @property + def latitude(self) -> Optional[float]: + """Return latitude value of this external event.""" + return None + + @property + def longitude(self) -> Optional[float]: + """Return longitude value of this external event.""" + return None + + @property + def state_attributes(self): + """Return the state attributes of this external event.""" + data = {} + if self.latitude is not None: + data[ATTR_LATITUDE] = round(self.latitude, 5) + if self.longitude is not None: + data[ATTR_LONGITUDE] = round(self.longitude, 5) + return data diff --git a/homeassistant/components/geo_location/demo.py b/homeassistant/components/geo_location/demo.py new file mode 100644 index 00000000000..8e8d8211086 --- /dev/null +++ b/homeassistant/components/geo_location/demo.py @@ -0,0 +1,132 @@ +""" +Demo platform for the geo location component. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/demo/ +""" +import logging +import random +from datetime import timedelta +from math import pi, cos, sin, radians + +from typing import Optional + +from homeassistant.components.geo_location import GeoLocationEvent +from homeassistant.helpers.event import track_time_interval + +_LOGGER = logging.getLogger(__name__) + +AVG_KM_PER_DEGREE = 111.0 +DEFAULT_UNIT_OF_MEASUREMENT = "km" +DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1) +MAX_RADIUS_IN_KM = 50 +NUMBER_OF_DEMO_DEVICES = 5 + +EVENT_NAMES = ["Bushfire", "Hazard Reduction", "Grass Fire", "Burn off", + "Structure Fire", "Fire Alarm", "Thunderstorm", "Tornado", + "Cyclone", "Waterspout", "Dust Storm", "Blizzard", "Ice Storm", + "Earthquake", "Tsunami"] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Demo geo locations.""" + DemoManager(hass, add_entities) + + +class DemoManager: + """Device manager for demo geo location events.""" + + def __init__(self, hass, add_entities): + """Initialise the demo geo location event manager.""" + self._hass = hass + self._add_entities = add_entities + self._managed_devices = [] + self._update(count=NUMBER_OF_DEMO_DEVICES) + self._init_regular_updates() + + def _generate_random_event(self): + """Generate a random event in vicinity of this HA instance.""" + home_latitude = self._hass.config.latitude + home_longitude = self._hass.config.longitude + + # Approx. 111km per degree (north-south). + radius_in_degrees = random.random() * MAX_RADIUS_IN_KM / \ + AVG_KM_PER_DEGREE + radius_in_km = radius_in_degrees * AVG_KM_PER_DEGREE + angle = random.random() * 2 * pi + # Compute coordinates based on radius and angle. Adjust longitude value + # based on HA's latitude. + latitude = home_latitude + radius_in_degrees * sin(angle) + longitude = home_longitude + radius_in_degrees * cos(angle) / \ + cos(radians(home_latitude)) + + event_name = random.choice(EVENT_NAMES) + return DemoGeoLocationEvent(event_name, radius_in_km, latitude, + longitude, DEFAULT_UNIT_OF_MEASUREMENT) + + def _init_regular_updates(self): + """Schedule regular updates based on configured time interval.""" + track_time_interval(self._hass, lambda now: self._update(), + DEFAULT_UPDATE_INTERVAL) + + def _update(self, count=1): + """Remove events and add new random events.""" + # Remove devices. + for _ in range(1, count + 1): + if self._managed_devices: + device = random.choice(self._managed_devices) + if device: + _LOGGER.debug("Removing %s", device) + self._managed_devices.remove(device) + self._hass.add_job(device.async_remove()) + # Generate new devices from events. + new_devices = [] + for _ in range(1, count + 1): + new_device = self._generate_random_event() + _LOGGER.debug("Adding %s", new_device) + new_devices.append(new_device) + self._managed_devices.append(new_device) + self._add_entities(new_devices) + + +class DemoGeoLocationEvent(GeoLocationEvent): + """This represents a demo geo location event.""" + + def __init__(self, name, distance, latitude, longitude, + unit_of_measurement): + """Initialize entity with data provided.""" + self._name = name + self._distance = distance + self._latitude = latitude + self._longitude = longitude + self._unit_of_measurement = unit_of_measurement + + @property + def name(self) -> Optional[str]: + """Return the name of the event.""" + return self._name + + @property + def should_poll(self): + """No polling needed for a demo geo location event.""" + return False + + @property + def distance(self) -> Optional[float]: + """Return distance value of this external event.""" + return self._distance + + @property + def latitude(self) -> Optional[float]: + """Return latitude value of this external event.""" + return self._latitude + + @property + def longitude(self) -> Optional[float]: + """Return longitude value of this external event.""" + return self._longitude + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement diff --git a/tests/components/geo_location/__init__.py b/tests/components/geo_location/__init__.py new file mode 100644 index 00000000000..56fc7d9fc92 --- /dev/null +++ b/tests/components/geo_location/__init__.py @@ -0,0 +1 @@ +"""The tests for Geo Location platforms.""" diff --git a/tests/components/geo_location/test_demo.py b/tests/components/geo_location/test_demo.py new file mode 100644 index 00000000000..158e5d61968 --- /dev/null +++ b/tests/components/geo_location/test_demo.py @@ -0,0 +1,63 @@ +"""The tests for the demo platform.""" +import unittest +from unittest.mock import patch + +from homeassistant.components import geo_location +from homeassistant.components.geo_location.demo import \ + NUMBER_OF_DEMO_DEVICES, DEFAULT_UNIT_OF_MEASUREMENT, \ + DEFAULT_UPDATE_INTERVAL +from homeassistant.setup import setup_component +from tests.common import get_test_home_assistant, assert_setup_component, \ + fire_time_changed +import homeassistant.util.dt as dt_util + +CONFIG = { + geo_location.DOMAIN: [ + { + 'platform': 'demo' + } + ] +} + + +class TestDemoPlatform(unittest.TestCase): + """Test the demo platform.""" + + def setUp(self): + """Initialize values for this testcase class.""" + self.hass = get_test_home_assistant() + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + def test_setup_platform(self): + """Test setup of demo platform via configuration.""" + utcnow = dt_util.utcnow() + # Patching 'utcnow' to gain more control over the timed update. + with patch('homeassistant.util.dt.utcnow', return_value=utcnow): + with assert_setup_component(1, geo_location.DOMAIN): + self.assertTrue(setup_component(self.hass, geo_location.DOMAIN, + CONFIG)) + + # In this test, only entities of the geo location domain have been + # generated. + all_states = self.hass.states.all() + assert len(all_states) == NUMBER_OF_DEMO_DEVICES + + # Check a single device's attributes. + state_first_entry = all_states[0] + self.assertAlmostEqual(state_first_entry.attributes['latitude'], + self.hass.config.latitude, delta=1.0) + self.assertAlmostEqual(state_first_entry.attributes['longitude'], + self.hass.config.longitude, delta=1.0) + assert state_first_entry.attributes['unit_of_measurement'] == \ + DEFAULT_UNIT_OF_MEASUREMENT + # Update (replaces 1 device). + fire_time_changed(self.hass, utcnow + DEFAULT_UPDATE_INTERVAL) + self.hass.block_till_done() + # Get all states again, ensure that the number of states is still + # the same, but the lists are different. + all_states_updated = self.hass.states.all() + assert len(all_states_updated) == NUMBER_OF_DEMO_DEVICES + self.assertNotEqual(all_states, all_states_updated) diff --git a/tests/components/geo_location/test_init.py b/tests/components/geo_location/test_init.py new file mode 100644 index 00000000000..54efe977bf9 --- /dev/null +++ b/tests/components/geo_location/test_init.py @@ -0,0 +1,20 @@ +"""The tests for the geo location component.""" +from homeassistant.components import geo_location +from homeassistant.components.geo_location import GeoLocationEvent +from homeassistant.setup import async_setup_component + + +async def test_setup_component(hass): + """Simple test setup of component.""" + result = await async_setup_component(hass, geo_location.DOMAIN) + assert result + + +async def test_event(hass): + """Simple test of the geo location event class.""" + entity = GeoLocationEvent() + + assert entity.state is None + assert entity.distance is None + assert entity.latitude is None + assert entity.longitude is None From 3cbf8e4f87e6701f5ba69fd729c99894eedf03b6 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Thu, 30 Aug 2018 19:21:37 +0300 Subject: [PATCH 070/172] Bump songpal dependency (#16297) Fixes #14936 --- homeassistant/components/media_player/songpal.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/songpal.py b/homeassistant/components/media_player/songpal.py index c1bfbbe59cd..e45819428e8 100644 --- a/homeassistant/components/media_player/songpal.py +++ b/homeassistant/components/media_player/songpal.py @@ -17,7 +17,7 @@ from homeassistant.const import ( from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-songpal==0.0.7'] +REQUIREMENTS = ['python-songpal==0.0.8'] SUPPORT_SONGPAL = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP | \ SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE | \ diff --git a/requirements_all.txt b/requirements_all.txt index 54d902f4260..327dd974edb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1130,7 +1130,7 @@ python-roku==3.1.5 python-sochain-api==0.0.2 # homeassistant.components.media_player.songpal -python-songpal==0.0.7 +python-songpal==0.0.8 # homeassistant.components.sensor.synologydsm python-synology==0.2.0 From 67d8db2c9f039bea7a0e44ee1d7ba34446faa8e2 Mon Sep 17 00:00:00 2001 From: PhracturedBlue Date: Thu, 30 Aug 2018 09:44:37 -0700 Subject: [PATCH 071/172] Use asterisk_mbox 0.5.0 client (#16296) --- homeassistant/components/asterisk_mbox.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/asterisk_mbox.py b/homeassistant/components/asterisk_mbox.py index e273d7d6f6a..0d6d811db70 100644 --- a/homeassistant/components/asterisk_mbox.py +++ b/homeassistant/components/asterisk_mbox.py @@ -15,7 +15,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send) -REQUIREMENTS = ['asterisk_mbox==0.4.0'] +REQUIREMENTS = ['asterisk_mbox==0.5.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 327dd974edb..1b8732d53ff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -139,7 +139,7 @@ apcaccess==0.0.13 apns2==0.3.0 # homeassistant.components.asterisk_mbox -asterisk_mbox==0.4.0 +asterisk_mbox==0.5.0 # homeassistant.components.media_player.dlna_dmr async-upnp-client==0.12.4 From b43c47cb177611b6d5be08a9ca0679ee4d6f7077 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 31 Aug 2018 10:17:11 +0200 Subject: [PATCH 072/172] Fix LIFX effects (#16309) --- CODEOWNERS | 3 + homeassistant/components/light/lifx.py | 125 ++++++++++++------------- 2 files changed, 64 insertions(+), 64 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index c756cb383d4..b86e09a6b72 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -52,6 +52,8 @@ homeassistant/components/cover/template.py @PhracturedBlue homeassistant/components/device_tracker/automatic.py @armills homeassistant/components/device_tracker/tile.py @bachya homeassistant/components/history_graph.py @andrey-git +homeassistant/components/light/lifx.py @amelchio +homeassistant/components/light/lifx_legacy.py @amelchio homeassistant/components/light/tplink.py @rytilahti homeassistant/components/light/yeelight.py @rytilahti homeassistant/components/lock/nello.py @pschmitt @@ -65,6 +67,7 @@ homeassistant/components/media_player/sonos.py @amelchio homeassistant/components/media_player/xiaomi_tv.py @fattdev homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth homeassistant/components/plant.py @ChristianKuehnel +homeassistant/components/scene/lifx_cloud.py @amelchio homeassistant/components/sensor/airvisual.py @bachya homeassistant/components/sensor/filter.py @dgomes homeassistant/components/sensor/gearbest.py @HerrHofrat diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index cf5d6fef704..bea39354e1b 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -167,9 +167,9 @@ async def async_setup_platform(hass, return True -def lifx_features(device): - """Return a feature map for this device, or a default map if unknown.""" - return aiolifx().products.features_map.get(device.product) or \ +def lifx_features(bulb): + """Return a feature map for this bulb, or a default map if unknown.""" + return aiolifx().products.features_map.get(bulb.product) or \ aiolifx().products.features_map.get(1) @@ -256,7 +256,7 @@ class LIFXManager: async def start_effect(self, entities, service, **kwargs): """Start a light effect on entities.""" - devices = [light.device for light in entities] + bulbs = [light.bulb for light in entities] if service == SERVICE_EFFECT_PULSE: effect = aiolifx_effects().EffectPulse( @@ -266,7 +266,7 @@ class LIFXManager: mode=kwargs.get(ATTR_MODE), hsbk=find_hsbk(**kwargs), ) - await self.effects_conductor.start(effect, devices) + await self.effects_conductor.start(effect, bulbs) elif service == SERVICE_EFFECT_COLORLOOP: preprocess_turn_on_alternatives(kwargs) @@ -282,12 +282,12 @@ class LIFXManager: transition=kwargs.get(ATTR_TRANSITION), brightness=brightness, ) - await self.effects_conductor.start(effect, devices) + await self.effects_conductor.start(effect, bulbs) elif service == SERVICE_EFFECT_STOP: - await self.effects_conductor.stop(devices) + await self.effects_conductor.stop(bulbs) def service_to_entities(self, service): - """Return the known devices that a service call mentions.""" + """Return the known entities that a service call mentions.""" entity_ids = extract_entity_ids(self.hass, service) if entity_ids: entities = [entity for entity in self.entities.values() @@ -298,50 +298,50 @@ class LIFXManager: return entities @callback - def register(self, device): + def register(self, bulb): """Handle aiolifx detected bulb.""" - self.hass.async_add_job(self.register_new_device(device)) + self.hass.async_add_job(self.register_new_bulb(bulb)) - async def register_new_device(self, device): + async def register_new_bulb(self, bulb): """Handle newly detected bulb.""" - if device.mac_addr in self.entities: - entity = self.entities[device.mac_addr] + if bulb.mac_addr in self.entities: + entity = self.entities[bulb.mac_addr] entity.registered = True _LOGGER.debug("%s register AGAIN", entity.who) await entity.update_hass() else: - _LOGGER.debug("%s register NEW", device.ip_addr) + _LOGGER.debug("%s register NEW", bulb.ip_addr) # Read initial state ack = AwaitAioLIFX().wait - color_resp = await ack(device.get_color) + color_resp = await ack(bulb.get_color) if color_resp: - version_resp = await ack(device.get_version) + version_resp = await ack(bulb.get_version) if color_resp is None or version_resp is None: - _LOGGER.error("Failed to initialize %s", device.ip_addr) - device.registered = False + _LOGGER.error("Failed to initialize %s", bulb.ip_addr) + bulb.registered = False else: - device.timeout = MESSAGE_TIMEOUT - device.retry_count = MESSAGE_RETRIES - device.unregister_timeout = UNAVAILABLE_GRACE + bulb.timeout = MESSAGE_TIMEOUT + bulb.retry_count = MESSAGE_RETRIES + bulb.unregister_timeout = UNAVAILABLE_GRACE - if lifx_features(device)["multizone"]: - entity = LIFXStrip(device, self.effects_conductor) - elif lifx_features(device)["color"]: - entity = LIFXColor(device, self.effects_conductor) + if lifx_features(bulb)["multizone"]: + entity = LIFXStrip(bulb, self.effects_conductor) + elif lifx_features(bulb)["color"]: + entity = LIFXColor(bulb, self.effects_conductor) else: - entity = LIFXWhite(device, self.effects_conductor) + entity = LIFXWhite(bulb, self.effects_conductor) _LOGGER.debug("%s register READY", entity.who) - self.entities[device.mac_addr] = entity + self.entities[bulb.mac_addr] = entity self.async_add_entities([entity], True) @callback - def unregister(self, device): + def unregister(self, bulb): """Handle aiolifx disappearing bulbs.""" - if device.mac_addr in self.entities: - entity = self.entities[device.mac_addr] + if bulb.mac_addr in self.entities: + entity = self.entities[bulb.mac_addr] _LOGGER.debug("%s unregister", entity.who) entity.registered = False self.hass.async_add_job(entity.async_update_ha_state()) @@ -352,20 +352,17 @@ class AwaitAioLIFX: def __init__(self): """Initialize the wrapper.""" - self.device = None self.message = None self.event = asyncio.Event() @callback - def callback(self, device, message): + def callback(self, bulb, message): """Handle responses.""" - self.device = device self.message = message self.event.set() async def wait(self, method): """Call an aiolifx method and wait for its response.""" - self.device = None self.message = None self.event.clear() method(callb=self.callback) @@ -387,9 +384,9 @@ def convert_16_to_8(value): class LIFXLight(Light): """Representation of a LIFX light.""" - def __init__(self, device, effects_conductor): + def __init__(self, bulb, effects_conductor): """Initialize the light.""" - self.light = device + self.bulb = bulb self.effects_conductor = effects_conductor self.registered = True self.postponed_update = None @@ -397,34 +394,34 @@ class LIFXLight(Light): @property def available(self): - """Return the availability of the device.""" + """Return the availability of the bulb.""" return self.registered @property def unique_id(self): """Return a unique ID.""" - return self.light.mac_addr + return self.bulb.mac_addr @property def name(self): - """Return the name of the device.""" - return self.light.label + """Return the name of the bulb.""" + return self.bulb.label @property def who(self): - """Return a string identifying the device.""" - return "%s (%s)" % (self.light.ip_addr, self.name) + """Return a string identifying the bulb.""" + return "%s (%s)" % (self.bulb.ip_addr, self.name) @property def min_mireds(self): """Return the coldest color_temp that this light supports.""" - kelvin = lifx_features(self.light)['max_kelvin'] + kelvin = lifx_features(self.bulb)['max_kelvin'] return math.floor(color_util.color_temperature_kelvin_to_mired(kelvin)) @property def max_mireds(self): """Return the warmest color_temp that this light supports.""" - kelvin = lifx_features(self.light)['min_kelvin'] + kelvin = lifx_features(self.bulb)['min_kelvin'] return math.ceil(color_util.color_temperature_kelvin_to_mired(kelvin)) @property @@ -432,8 +429,8 @@ class LIFXLight(Light): """Flag supported features.""" support = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION | SUPPORT_EFFECT - device_features = lifx_features(self.light) - if device_features['min_kelvin'] != device_features['max_kelvin']: + bulb_features = lifx_features(self.bulb) + if bulb_features['min_kelvin'] != bulb_features['max_kelvin']: support |= SUPPORT_COLOR_TEMP return support @@ -441,25 +438,25 @@ class LIFXLight(Light): @property def brightness(self): """Return the brightness of this light between 0..255.""" - return convert_16_to_8(self.light.color[2]) + return convert_16_to_8(self.bulb.color[2]) @property def color_temp(self): """Return the color temperature.""" - _, sat, _, kelvin = self.light.color + _, sat, _, kelvin = self.bulb.color if sat: return None return color_util.color_temperature_kelvin_to_mired(kelvin) @property def is_on(self): - """Return true if device is on.""" - return self.light.power_level != 0 + """Return true if light is on.""" + return self.bulb.power_level != 0 @property def effect(self): """Return the name of the currently running effect.""" - effect = self.effects_conductor.effect(self.light) + effect = self.effects_conductor.effect(self.bulb) if effect: return 'lifx_effect_' + effect.name return None @@ -485,19 +482,19 @@ class LIFXLight(Light): util.dt.utcnow() + timedelta(milliseconds=when)) async def async_turn_on(self, **kwargs): - """Turn the device on.""" + """Turn the light on.""" kwargs[ATTR_POWER] = True self.hass.async_add_job(self.set_state(**kwargs)) async def async_turn_off(self, **kwargs): - """Turn the device off.""" + """Turn the light off.""" kwargs[ATTR_POWER] = False self.hass.async_add_job(self.set_state(**kwargs)) async def set_state(self, **kwargs): """Set a color on the light and turn it on/off.""" async with self.lock: - bulb = self.light + bulb = self.bulb await self.effects_conductor.stop([bulb]) @@ -544,13 +541,13 @@ class LIFXLight(Light): await self.update_during_transition(fade) async def set_power(self, ack, pwr, duration=0): - """Send a power change to the device.""" - await ack(partial(self.light.set_power, pwr, duration=duration)) + """Send a power change to the bulb.""" + await ack(partial(self.bulb.set_power, pwr, duration=duration)) async def set_color(self, ack, hsbk, kwargs, duration=0): - """Send a color change to the device.""" - hsbk = merge_hsbk(self.light.color, hsbk) - await ack(partial(self.light.set_color, hsbk, duration=duration)) + """Send a color change to the bulb.""" + hsbk = merge_hsbk(self.bulb.color, hsbk) + await ack(partial(self.bulb.set_color, hsbk, duration=duration)) async def default_effect(self, **kwargs): """Start an effect with default parameters.""" @@ -563,7 +560,7 @@ class LIFXLight(Light): async def async_update(self): """Update bulb status.""" if self.available and not self.lock.locked(): - await AwaitAioLIFX().wait(self.light.get_color) + await AwaitAioLIFX().wait(self.bulb.get_color) class LIFXWhite(LIFXLight): @@ -600,7 +597,7 @@ class LIFXColor(LIFXLight): @property def hs_color(self): """Return the hs value.""" - hue, sat, _, _ = self.light.color + hue, sat, _, _ = self.bulb.color hue = hue / 65535 * 360 sat = sat / 65535 * 100 return (hue, sat) if sat else None @@ -610,8 +607,8 @@ class LIFXStrip(LIFXColor): """Representation of a LIFX light strip with multiple zones.""" async def set_color(self, ack, hsbk, kwargs, duration=0): - """Send a color change to the device.""" - bulb = self.light + """Send a color change to the bulb.""" + bulb = self.bulb num_zones = len(bulb.color_zones) zones = kwargs.get(ATTR_ZONES) @@ -659,7 +656,7 @@ class LIFXStrip(LIFXColor): while self.available and zone < top: # Each get_color_zones can update 8 zones at once resp = await AwaitAioLIFX().wait(partial( - self.light.get_color_zones, + self.bulb.get_color_zones, start_index=zone)) if resp: zone += 8 From 26d39d39ea1277ed8ca0698e244bbed217dbded0 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Fri, 31 Aug 2018 20:54:25 +1000 Subject: [PATCH 073/172] avoid error in debug log mode and rss entry without title (#16316) --- homeassistant/components/feedreader.py | 4 ++-- tests/components/test_feedreader.py | 6 +++--- tests/fixtures/feedreader3.xml | 5 +++++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/feedreader.py b/homeassistant/components/feedreader.py index 782fd8ac8dd..7882cdc5a15 100644 --- a/homeassistant/components/feedreader.py +++ b/homeassistant/components/feedreader.py @@ -143,7 +143,7 @@ class FeedManager: else: self._has_published_parsed = False _LOGGER.debug("No published_parsed info available for entry %s", - entry.title) + entry) entry.update({'feed_url': self._url}) self._hass.bus.fire(self._event_type, entry) @@ -164,7 +164,7 @@ class FeedManager: self._update_and_fire_entry(entry) new_entries = True else: - _LOGGER.debug("Entry %s already processed", entry.title) + _LOGGER.debug("Entry %s already processed", entry) if not new_entries: self._log_no_entries() self._firstrun = False diff --git a/tests/components/test_feedreader.py b/tests/components/test_feedreader.py index dd98ebaf189..668f116362c 100644 --- a/tests/components/test_feedreader.py +++ b/tests/components/test_feedreader.py @@ -160,11 +160,11 @@ class TestFeedreaderComponent(unittest.TestCase): manager, events = self.setup_manager(feed_data, max_entries=5) assert len(events) == 5 - def test_feed_without_publication_date(self): - """Test simple feed with entry without publication date.""" + def test_feed_without_publication_date_and_title(self): + """Test simple feed with entry without publication date and title.""" feed_data = load_fixture('feedreader3.xml') manager, events = self.setup_manager(feed_data) - assert len(events) == 2 + assert len(events) == 3 def test_feed_invalid_data(self): """Test feed with invalid data.""" diff --git a/tests/fixtures/feedreader3.xml b/tests/fixtures/feedreader3.xml index 7b28e067cfe..d8ccd119306 100644 --- a/tests/fixtures/feedreader3.xml +++ b/tests/fixtures/feedreader3.xml @@ -21,6 +21,11 @@ http://www.example.com/link/2 GUID 2 + + Description 3 + http://www.example.com/link/3 + GUID 3 + From 93f45779c66b6984104eea7737588511767e41d7 Mon Sep 17 00:00:00 2001 From: lamiskin Date: Fri, 31 Aug 2018 20:57:07 +1000 Subject: [PATCH 074/172] Correct wemo static device discovery issue. (#16292) A recent change caused an issue if a single static wemo device is offline and could not be reached, then the whole component would not initialize (and therefore all other wemo devices are not added). --- homeassistant/components/wemo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/wemo.py b/homeassistant/components/wemo.py index 2ce2ff475a2..9664ca9419a 100644 --- a/homeassistant/components/wemo.py +++ b/homeassistant/components/wemo.py @@ -124,14 +124,14 @@ def setup(hass, config): _LOGGER.error( 'Unable to get description url for %s', '{}:{}'.format(host, port) if port else host) - return False + continue try: device = pywemo.discovery.device_from_description(url, None) except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as err: _LOGGER.error('Unable to access %s (%s)', url, err) - return False + continue devices.append((url, device)) From efa9c82c38e82cd7d680cb212f03174b571a17de Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 31 Aug 2018 12:59:39 +0200 Subject: [PATCH 075/172] Update frontend to 20180831.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index da3d225bba0..5508aa76acf 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -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==20180829.1'] +REQUIREMENTS = ['home-assistant-frontend==20180831.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 1b8732d53ff..0d0acdda32f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -448,7 +448,7 @@ hole==0.3.0 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180829.1 +home-assistant-frontend==20180831.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e0d1596db3f..bd921085461 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -84,7 +84,7 @@ hbmqtt==0.9.2 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180829.1 +home-assistant-frontend==20180831.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 5e8a1496d7c0fd371d88f959708cef06cdbde793 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 31 Aug 2018 13:23:22 +0200 Subject: [PATCH 076/172] Update translations --- .../components/auth/.translations/ar.json | 7 +++++ .../components/auth/.translations/de.json | 16 ++++++++++ .../components/auth/.translations/es-419.json | 12 +++++++ .../components/auth/.translations/fr.json | 16 ++++++++++ .../components/auth/.translations/it.json | 13 ++++++++ .../components/auth/.translations/ko.json | 4 +-- .../components/auth/.translations/nl.json | 16 ++++++++++ .../components/auth/.translations/no.json | 16 ++++++++++ .../components/auth/.translations/pl.json | 16 ++++++++++ .../components/auth/.translations/pt.json | 16 ++++++++++ .../components/auth/.translations/ru.json | 2 +- .../components/auth/.translations/sl.json | 16 ++++++++++ .../components/cast/.translations/fr.json | 13 ++++++++ .../components/deconz/.translations/nl.json | 2 +- .../components/hangouts/.translations/de.json | 1 + .../components/hangouts/.translations/en.json | 4 +-- .../hangouts/.translations/es-419.json | 18 +++++++++++ .../components/hangouts/.translations/fr.json | 21 +++++++++++++ .../components/hangouts/.translations/it.json | 24 ++++++++++++++ .../components/hangouts/.translations/nl.json | 29 +++++++++++++++++ .../components/hangouts/.translations/no.json | 18 ++++++++++- .../hangouts/.translations/pt-BR.json | 11 +------ .../components/hangouts/.translations/pt.json | 31 +++++++++++++++++++ .../.translations/es-419.json | 4 ++- .../homematicip_cloud/.translations/fr.json | 21 +++++++++++++ .../homematicip_cloud/.translations/it.json | 8 +++++ .../homematicip_cloud/.translations/nl.json | 8 ++--- .../homematicip_cloud/.translations/pt.json | 1 + .../components/hue/.translations/nl.json | 2 +- .../components/nest/.translations/fr.json | 7 +++++ .../sensor/.translations/moon.pt-BR.json | 2 -- .../sensor/.translations/moon.pt.json | 12 +++++++ .../components/sonos/.translations/de.json | 2 +- 33 files changed, 363 insertions(+), 26 deletions(-) create mode 100644 homeassistant/components/auth/.translations/ar.json create mode 100644 homeassistant/components/auth/.translations/de.json create mode 100644 homeassistant/components/auth/.translations/es-419.json create mode 100644 homeassistant/components/auth/.translations/fr.json create mode 100644 homeassistant/components/auth/.translations/it.json create mode 100644 homeassistant/components/auth/.translations/nl.json create mode 100644 homeassistant/components/auth/.translations/no.json create mode 100644 homeassistant/components/auth/.translations/pl.json create mode 100644 homeassistant/components/auth/.translations/pt.json create mode 100644 homeassistant/components/auth/.translations/sl.json create mode 100644 homeassistant/components/cast/.translations/fr.json create mode 100644 homeassistant/components/hangouts/.translations/es-419.json create mode 100644 homeassistant/components/hangouts/.translations/fr.json create mode 100644 homeassistant/components/hangouts/.translations/nl.json create mode 100644 homeassistant/components/hangouts/.translations/pt.json create mode 100644 homeassistant/components/homematicip_cloud/.translations/fr.json create mode 100644 homeassistant/components/nest/.translations/fr.json create mode 100644 homeassistant/components/sensor/.translations/moon.pt.json diff --git a/homeassistant/components/auth/.translations/ar.json b/homeassistant/components/auth/.translations/ar.json new file mode 100644 index 00000000000..1ef902e6fe2 --- /dev/null +++ b/homeassistant/components/auth/.translations/ar.json @@ -0,0 +1,7 @@ +{ + "mfa_setup": { + "totp": { + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/de.json b/homeassistant/components/auth/.translations/de.json new file mode 100644 index 00000000000..67f948e8340 --- /dev/null +++ b/homeassistant/components/auth/.translations/de.json @@ -0,0 +1,16 @@ +{ + "mfa_setup": { + "totp": { + "error": { + "invalid_code": "Ung\u00fcltiger Code, bitte versuche es erneut. Wenn Sie diesen Fehler regelm\u00e4\u00dfig erhalten, stelle sicher, dass die Uhr deines Home Assistant-Systems korrekt ist." + }, + "step": { + "init": { + "description": "Um die Zwei-Faktor-Authentifizierung mit zeitbasierten Einmalpassw\u00f6rtern zu aktivieren, scanne den QR-Code mit Ihrer Authentifizierungs-App. Wenn du keine hast, empfehlen wir entweder [Google Authenticator] (https://support.google.com/accounts/answer/1066447) oder [Authy] (https://authy.com/). \n\n {qr_code} \n \nNachdem du den Code gescannt hast, gebe den sechsstelligen Code aus der App ein, um das Setup zu \u00fcberpr\u00fcfen. Wenn es Probleme beim Scannen des QR-Codes gibt, f\u00fchre ein manuelles Setup mit dem Code ** ` {code} ` ** durch.", + "title": "Richte die Zwei-Faktor-Authentifizierung mit TOTP ein" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/es-419.json b/homeassistant/components/auth/.translations/es-419.json new file mode 100644 index 00000000000..6caa9d49993 --- /dev/null +++ b/homeassistant/components/auth/.translations/es-419.json @@ -0,0 +1,12 @@ +{ + "mfa_setup": { + "totp": { + "step": { + "init": { + "title": "Configurar la autenticaci\u00f3n de dos factores mediante TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/fr.json b/homeassistant/components/auth/.translations/fr.json new file mode 100644 index 00000000000..e8a8037c39a --- /dev/null +++ b/homeassistant/components/auth/.translations/fr.json @@ -0,0 +1,16 @@ +{ + "mfa_setup": { + "totp": { + "error": { + "invalid_code": "Code invalide. S'il vous pla\u00eet essayez \u00e0 nouveau. Si cette erreur persiste, assurez-vous que l'horloge de votre syst\u00e8me Home Assistant est correcte." + }, + "step": { + "init": { + "description": "Pour activer l'authentification \u00e0 deux facteurs \u00e0 l'aide de mots de passe \u00e0 utilisation unique bas\u00e9s sur l'heure, num\u00e9risez le code QR avec votre application d'authentification. Si vous n'en avez pas, nous vous recommandons d'utiliser [Google Authenticator] (https://support.google.com/accounts/answer/1066447) ou [Authy] (https://authy.com/). \n\n {qr_code} \n \n Apr\u00e8s avoir num\u00e9ris\u00e9 le code, entrez le code \u00e0 six chiffres de votre application pour v\u00e9rifier la configuration. Si vous rencontrez des probl\u00e8mes lors de l\u2019analyse du code QR, effectuez une configuration manuelle avec le code ** ` {code} ` **.", + "title": "Configurer une authentification \u00e0 deux facteurs \u00e0 l'aide de TOTP" + } + }, + "title": "TOTP (Mot de passe \u00e0 utilisation unique bas\u00e9 sur le temps)" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/it.json b/homeassistant/components/auth/.translations/it.json new file mode 100644 index 00000000000..869c3b438af --- /dev/null +++ b/homeassistant/components/auth/.translations/it.json @@ -0,0 +1,13 @@ +{ + "mfa_setup": { + "totp": { + "step": { + "init": { + "description": "Per attivare l'autenticazione a due fattori utilizzando password monouso basate sul tempo, eseguire la scansione del codice QR con l'app di autenticazione. Se non ne hai uno, ti consigliamo [Google Authenticator] (https://support.google.com/accounts/answer/1066447) o [Authy] (https://authy.com/). \n\n {qr_code} \n \n Dopo aver scansionato il codice, inserisci il codice a sei cifre dalla tua app per verificare la configurazione. Se riscontri problemi con la scansione del codice QR, esegui una configurazione manuale con codice ** ` {code} ` **.", + "title": "Imposta l'autenticazione a due fattori usando TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/ko.json b/homeassistant/components/auth/.translations/ko.json index 726fa6a6cd1..4eb4783edd9 100644 --- a/homeassistant/components/auth/.translations/ko.json +++ b/homeassistant/components/auth/.translations/ko.json @@ -2,11 +2,11 @@ "mfa_setup": { "totp": { "error": { - "invalid_code": "\uc798\ubabb\ub41c \ucf54\ub4dc \uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694. \uc774 \uc624\ub958\uac00 \uc9c0\uc18d\uc801\uc73c\ub85c \ubc1c\uc0dd\ud55c\ub2e4\uba74 Home Assistant \uc758 \uc2dc\uacc4\uac00 \uc815\ud655\ud55c\uc9c0 \ud655\uc778\ud574\ubcf4\uc138\uc694." + "invalid_code": "\uc798\ubabb\ub41c \ucf54\ub4dc \uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694. \uc774 \uc624\ub958\uac00 \uc9c0\uc18d\uc801\uc73c\ub85c \ubc1c\uc0dd\ud55c\ub2e4\uba74 Home Assistant \uc758 \uc2dc\uac04\uc124\uc815\uc774 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud574\ubcf4\uc138\uc694." }, "step": { "init": { - "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574 \uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \ub098 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.", + "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574 \uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.", "title": "TOTP \ub97c \uc0ac\uc6a9\ud558\uc5ec 2 \ub2e8\uacc4 \uc778\uc99d \uad6c\uc131" } }, diff --git a/homeassistant/components/auth/.translations/nl.json b/homeassistant/components/auth/.translations/nl.json new file mode 100644 index 00000000000..40a873023dd --- /dev/null +++ b/homeassistant/components/auth/.translations/nl.json @@ -0,0 +1,16 @@ +{ + "mfa_setup": { + "totp": { + "error": { + "invalid_code": "Ongeldige code, probeer het opnieuw. Als u deze fout blijft krijgen, controleer dan of de klok van uw Home Assistant systeem correct is ingesteld." + }, + "step": { + "init": { + "description": "Voor het activeren van twee-factor-authenticatie via tijdgebonden eenmalige wachtwoorden: scan de QR code met uw authenticatie-app. Als u nog geen app heeft, adviseren we [Google Authenticator (https://support.google.com/accounts/answer/1066447) of [Authy](https://authy.com/).\n\n{qr_code}\n\nNa het scannen van de code voert u de zescijferige code uit uw app in om de instelling te controleren. Als u problemen heeft met het scannen van de QR-code, voert u een handmatige configuratie uit met code **`{code}`**.", + "title": "Configureer twee-factor-authenticatie via TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/no.json b/homeassistant/components/auth/.translations/no.json new file mode 100644 index 00000000000..43ec497cfb1 --- /dev/null +++ b/homeassistant/components/auth/.translations/no.json @@ -0,0 +1,16 @@ +{ + "mfa_setup": { + "totp": { + "error": { + "invalid_code": "Ugyldig kode, pr\u00f8v igjen. Hvis du f\u00e5r denne feilen konsekvent, m\u00e5 du s\u00f8rge for at klokken p\u00e5 Home Assistant systemet er riktig." + }, + "step": { + "init": { + "description": "For \u00e5 aktivere tofaktorautentisering ved hjelp av tidsbaserte engangspassord, skann QR-koden med autentiseringsappen din. Hvis du ikke har en, kan vi anbefale enten [Google Authenticator](https://support.google.com/accounts/answer/1066447) eller [Authy](https://authy.com/). \n\n {qr_code} \n \nEtter at du har skannet koden, skriver du inn den seks-sifrede koden fra appen din for \u00e5 kontrollere oppsettet. Dersom du har problemer med \u00e5 skanne QR-koden kan du taste inn f\u00f8lgende kode manuelt: **`{code}`**.", + "title": "Konfigurer tofaktorautentisering ved hjelp av TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/pl.json b/homeassistant/components/auth/.translations/pl.json new file mode 100644 index 00000000000..78999c34c22 --- /dev/null +++ b/homeassistant/components/auth/.translations/pl.json @@ -0,0 +1,16 @@ +{ + "mfa_setup": { + "totp": { + "error": { + "invalid_code": "Nieprawid\u0142owy kod, spr\u00f3buj ponownie. Je\u015bli b\u0142\u0105d b\u0119dzie si\u0119 powtarza\u0142, upewnij si\u0119, \u017ce czas zegara systemu Home Assistant jest prawid\u0142owy." + }, + "step": { + "init": { + "description": "Aby aktywowa\u0107 uwierzytelnianie dwusk\u0142adnikowe przy u\u017cyciu jednorazowych hase\u0142 opartych na czasie, zeskanuj kod QR za pomoc\u0105 aplikacji uwierzytelniaj\u0105cej. Je\u015bli jej nie masz, polecamy [Google Authenticator](https://support.google.com/accounts/answer/1066447) lub [Authy](https://authy.com/).\n\n{qr_code} \n \nPo zeskanowaniu kodu wprowad\u017a sze\u015bciocyfrowy kod z aplikacji, aby zweryfikowa\u0107 konfiguracj\u0119. Je\u015bli masz problemy z zeskanowaniem kodu QR, wykonaj r\u0119czn\u0105 konfiguracj\u0119 z kodem **`{code}`**.", + "title": "Skonfiguruj uwierzytelnianie dwusk\u0142adnikowe za pomoc\u0105 hase\u0142 jednorazowych opartych na czasie" + } + }, + "title": "Has\u0142a jednorazowe oparte na czasie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/pt.json b/homeassistant/components/auth/.translations/pt.json new file mode 100644 index 00000000000..474dbe488be --- /dev/null +++ b/homeassistant/components/auth/.translations/pt.json @@ -0,0 +1,16 @@ +{ + "mfa_setup": { + "totp": { + "error": { + "invalid_code": "C\u00f3digo inv\u00e1lido, por favor, tente novamente. Se receber este erro constantemente, por favor, certifique-se de que o rel\u00f3gio do sistema que hospeda o Home Assistent \u00e9 preciso." + }, + "step": { + "init": { + "description": "Para ativar a autentica\u00e7\u00e3o com dois fatores utilizando passwords unicas temporais (OTP), ler o c\u00f3digo QR com a sua aplica\u00e7\u00e3o de autentica\u00e7\u00e3o. Se voc\u00ea n\u00e3o tiver uma, recomendamos [Google Authenticator](https://support.google.com/accounts/answer/1066447) ou [Authy](https://authy.com/).\n\n{qr_code}\n\nDepois de ler o c\u00f3digo, introduza o c\u00f3digo de seis d\u00edgitos fornecido pela sua aplica\u00e7\u00e3o para verificar a configura\u00e7\u00e3o. Se tiver problemas a ler o c\u00f3digo QR, fa\u00e7a uma configura\u00e7\u00e3o manual com o c\u00f3digo **`{c\u00f3digo}`**.", + "title": "Configurar autentica\u00e7\u00e3o com dois fatores usando TOTP" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/ru.json b/homeassistant/components/auth/.translations/ru.json index b4b5b58f9fa..a716425f345 100644 --- a/homeassistant/components/auth/.translations/ru.json +++ b/homeassistant/components/auth/.translations/ru.json @@ -6,7 +6,7 @@ }, "step": { "init": { - "description": "\u0427\u0442\u043e\u0431\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0443\u044e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0445 \u043f\u0430\u0440\u043e\u043b\u0435\u0439, \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \u043d\u0430 \u0432\u0440\u0435\u043c\u0435\u043d\u0438, \u043e\u0442\u0441\u043a\u0430\u043d\u0438\u0440\u0443\u0439\u0442\u0435 QR-\u043a\u043e\u0434 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043f\u043e\u0434\u043b\u0438\u043d\u043d\u043e\u0441\u0442\u0438. \u0415\u0441\u043b\u0438 \u0443 \u0432\u0430\u0441 \u0435\u0433\u043e \u043d\u0435\u0442, \u043c\u044b \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043b\u0438\u0431\u043e [Google Authenticator] (https://support.google.com/accounts/answer/1066447), \u043b\u0438\u0431\u043e [Authy] (https://authy.com/). \n\n {qr_code} \n \n \u041f\u043e\u0441\u043b\u0435 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f QR-\u043a\u043e\u0434\u0430 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0448\u0435\u0441\u0442\u0438\u0437\u043d\u0430\u0447\u043d\u044b\u0439 \u043a\u043e\u0434 \u0438\u0437 \u0432\u0430\u0448\u0435\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u0415\u0441\u043b\u0438 \u0443 \u0432\u0430\u0441 \u0435\u0441\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441\u043e \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\u043c QR-\u043a\u043e\u0434\u0430, \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0440\u0443\u0447\u043d\u0443\u044e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0441 \u043a\u043e\u0434\u043e\u043c ** ` {code} ` **.", + "description": "\u0427\u0442\u043e\u0431\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0443\u044e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0445 \u043f\u0430\u0440\u043e\u043b\u0435\u0439, \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \u043d\u0430 \u0432\u0440\u0435\u043c\u0435\u043d\u0438, \u043e\u0442\u0441\u043a\u0430\u043d\u0438\u0440\u0443\u0439\u0442\u0435 QR-\u043a\u043e\u0434 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043f\u043e\u0434\u043b\u0438\u043d\u043d\u043e\u0441\u0442\u0438. \u0415\u0441\u043b\u0438 \u0443 \u0432\u0430\u0441 \u0435\u0433\u043e \u043d\u0435\u0442, \u043c\u044b \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043b\u0438\u0431\u043e [Google Authenticator](https://support.google.com/accounts/answer/1066447), \u043b\u0438\u0431\u043e [Authy](https://authy.com/). \n\n {qr_code} \n \n\u041f\u043e\u0441\u043b\u0435 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f QR-\u043a\u043e\u0434\u0430 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0448\u0435\u0441\u0442\u0438\u0437\u043d\u0430\u0447\u043d\u044b\u0439 \u043a\u043e\u0434 \u0438\u0437 \u0432\u0430\u0448\u0435\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u0415\u0441\u043b\u0438 \u0443 \u0432\u0430\u0441 \u0435\u0441\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441\u043e \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\u043c QR-\u043a\u043e\u0434\u0430, \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043a\u043e\u0434\u0430 **`{code}`**.", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c TOTP" } }, diff --git a/homeassistant/components/auth/.translations/sl.json b/homeassistant/components/auth/.translations/sl.json new file mode 100644 index 00000000000..45b57a772f9 --- /dev/null +++ b/homeassistant/components/auth/.translations/sl.json @@ -0,0 +1,16 @@ +{ + "mfa_setup": { + "totp": { + "error": { + "invalid_code": "Neveljavna koda, prosimo, poskusite znova. \u010ce dobite to sporo\u010dilo ve\u010dkrat, prosimo poskrbite, da bo ura va\u0161ega Home Assistenta to\u010dna." + }, + "step": { + "init": { + "description": "\u010ce \u017eelite aktivirati preverjanje pristnosti dveh faktorjev z enkratnimi gesli, ki temeljijo na \u010dasu, skenirajte kodo QR s svojo aplikacijo za preverjanje pristnosti. \u010ce je nimate, priporo\u010damo bodisi [Google Authenticator] (https://support.google.com/accounts/answer/1066447) ali [Authy] (https://authy.com/). \n\n {qr_code} \n \n Po skeniranju kode vnesite \u0161estmestno kodo iz aplikacije, da preverite nastavitev. \u010ce imate te\u017eave pri skeniranju kode QR, naredite ro\u010dno nastavitev s kodo ** ` {code} ` **.", + "title": "Nastavite dvofaktorsko avtentifikacijo s pomo\u010djo TOTP-ja" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/fr.json b/homeassistant/components/cast/.translations/fr.json new file mode 100644 index 00000000000..acacddf2187 --- /dev/null +++ b/homeassistant/components/cast/.translations/fr.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Aucun appareil Google Cast trouv\u00e9 sur le r\u00e9seau.", + "single_instance_allowed": "Seulement une seule configuration de Google Cast est n\u00e9cessaire." + }, + "step": { + "confirm": { + "description": "Voulez-vous configurer Google Cast?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/nl.json b/homeassistant/components/deconz/.translations/nl.json index 6f3fa2ec9a4..9084d22f4a3 100644 --- a/homeassistant/components/deconz/.translations/nl.json +++ b/homeassistant/components/deconz/.translations/nl.json @@ -28,6 +28,6 @@ "title": "Extra configuratieopties voor deCONZ" } }, - "title": "deCONZ" + "title": "deCONZ Zigbee gateway" } } \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/de.json b/homeassistant/components/hangouts/.translations/de.json index 4222e7f5556..a2ed8d21230 100644 --- a/homeassistant/components/hangouts/.translations/de.json +++ b/homeassistant/components/hangouts/.translations/de.json @@ -5,6 +5,7 @@ "unknown": "Ein unbekannter Fehler ist aufgetreten." }, "error": { + "invalid_2fa": "Ung\u00fcltige 2-Faktor Authentifizierung, bitte versuche es erneut.", "invalid_2fa_method": "Ung\u00fcltige 2FA Methode (mit Telefon verifizieren)", "invalid_login": "Ung\u00fcltige Daten, bitte erneut versuchen." }, diff --git a/homeassistant/components/hangouts/.translations/en.json b/homeassistant/components/hangouts/.translations/en.json index 6e70a1f4310..f526bec4f34 100644 --- a/homeassistant/components/hangouts/.translations/en.json +++ b/homeassistant/components/hangouts/.translations/en.json @@ -5,7 +5,7 @@ "unknown": "Unknown error occurred." }, "error": { - "invalid_2fa": "Invalid 2 Factor Authorization, please try again.", + "invalid_2fa": "Invalid 2 Factor Authentication, please try again.", "invalid_2fa_method": "Invalid 2FA Method (Verify on Phone).", "invalid_login": "Invalid Login, please try again." }, @@ -14,7 +14,7 @@ "data": { "2fa": "2FA Pin" }, - "title": "2-Factor-Authorization" + "title": "2-Factor-Authentication" }, "user": { "data": { diff --git a/homeassistant/components/hangouts/.translations/es-419.json b/homeassistant/components/hangouts/.translations/es-419.json new file mode 100644 index 00000000000..a3699db08ae --- /dev/null +++ b/homeassistant/components/hangouts/.translations/es-419.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Google Hangouts ya est\u00e1 configurado", + "unknown": "Se produjo un error desconocido." + }, + "step": { + "user": { + "data": { + "email": "Direcci\u00f3n de correo electr\u00f3nico", + "password": "Contrase\u00f1a" + }, + "title": "Inicio de sesi\u00f3n de Google Hangouts" + } + }, + "title": "Google Hangouts" + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/fr.json b/homeassistant/components/hangouts/.translations/fr.json new file mode 100644 index 00000000000..c92d478c454 --- /dev/null +++ b/homeassistant/components/hangouts/.translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Google Hangouts est d\u00e9j\u00e0 configur\u00e9", + "unknown": "Une erreur inconnue s'est produite" + }, + "error": { + "invalid_login": "Login invalide, veuillez r\u00e9essayer." + }, + "step": { + "2fa": { + "title": "Authentification \u00e0 2 facteurs" + }, + "user": { + "data": { + "password": "Mot de passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/it.json b/homeassistant/components/hangouts/.translations/it.json index 0c609b3430a..76a9adcb40e 100644 --- a/homeassistant/components/hangouts/.translations/it.json +++ b/homeassistant/components/hangouts/.translations/it.json @@ -1,5 +1,29 @@ { "config": { + "abort": { + "already_configured": "Google Hangouts \u00e8 gi\u00e0 configurato", + "unknown": "Si \u00e8 verificato un errore sconosciuto." + }, + "error": { + "invalid_2fa": "Autenticazione a 2 fattori non valida, riprovare.", + "invalid_2fa_method": "Metodo 2FA non valido (verifica sul telefono).", + "invalid_login": "Accesso non valido, si prega di riprovare." + }, + "step": { + "2fa": { + "data": { + "2fa": "2FA Pin" + }, + "title": "Autenticazione a due fattori" + }, + "user": { + "data": { + "email": "Indirizzo email", + "password": "Password" + }, + "title": "Accesso a Google Hangouts" + } + }, "title": "Google Hangouts" } } \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/nl.json b/homeassistant/components/hangouts/.translations/nl.json new file mode 100644 index 00000000000..cf73210aa3b --- /dev/null +++ b/homeassistant/components/hangouts/.translations/nl.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Google Hangouts is al geconfigureerd", + "unknown": "Onbekende fout opgetreden." + }, + "error": { + "invalid_2fa": "Ongeldige twee-factor-authenticatie, probeer het opnieuw.", + "invalid_2fa_method": "Ongeldige 2FA-methode (verifi\u00ebren op telefoon).", + "invalid_login": "Ongeldige aanmelding, probeer het opnieuw." + }, + "step": { + "2fa": { + "data": { + "2fa": "2FA pin" + }, + "title": "Twee-factor-authenticatie" + }, + "user": { + "data": { + "email": "E-mailadres", + "password": "Wachtwoord" + }, + "title": "Google Hangouts inlog" + } + }, + "title": "Google Hangouts" + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/no.json b/homeassistant/components/hangouts/.translations/no.json index 7ea074470c7..c2cdb93c005 100644 --- a/homeassistant/components/hangouts/.translations/no.json +++ b/homeassistant/components/hangouts/.translations/no.json @@ -1,11 +1,27 @@ { "config": { + "abort": { + "already_configured": "Google Hangouts er allerede konfigurert", + "unknown": "Ukjent feil oppstod." + }, + "error": { + "invalid_2fa": "Ugyldig tofaktorautentisering, vennligst pr\u00f8v igjen.", + "invalid_2fa_method": "Ugyldig 2FA-metode (Bekreft p\u00e5 telefon).", + "invalid_login": "Ugyldig innlogging, vennligst pr\u00f8v igjen." + }, "step": { + "2fa": { + "data": { + "2fa": "2FA Pin" + }, + "title": "Tofaktorautentisering" + }, "user": { "data": { "email": "E-postadresse", "password": "Passord" - } + }, + "title": "Google Hangouts p\u00e5logging" } }, "title": "Google Hangouts" diff --git a/homeassistant/components/hangouts/.translations/pt-BR.json b/homeassistant/components/hangouts/.translations/pt-BR.json index 4dffe492c4d..41b097f3f8d 100644 --- a/homeassistant/components/hangouts/.translations/pt-BR.json +++ b/homeassistant/components/hangouts/.translations/pt-BR.json @@ -1,23 +1,14 @@ { "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." + "already_configured": "Hangouts do Google j\u00e1 est\u00e1 configurado." }, "step": { "2fa": { - "data": { - "2fa": "Pin 2FA" - }, "title": "" }, "user": { "data": { - "email": "Endere\u00e7o de e-mail", "password": "Senha" }, "title": "Login do Hangouts do Google" diff --git a/homeassistant/components/hangouts/.translations/pt.json b/homeassistant/components/hangouts/.translations/pt.json new file mode 100644 index 00000000000..64c960a121a --- /dev/null +++ b/homeassistant/components/hangouts/.translations/pt.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Google Hangouts j\u00e1 est\u00e1 configurado", + "unknown": "Ocorreu um erro desconhecido." + }, + "error": { + "invalid_2fa": "Autoriza\u00e7\u00e3o por 2 factores inv\u00e1lida, por favor, tente novamente.", + "invalid_2fa_method": "M\u00e9todo 2FA inv\u00e1lido (verificar no telefone).", + "invalid_login": "Login inv\u00e1lido, por favor, tente novamente." + }, + "step": { + "2fa": { + "data": { + "2fa": "Pin 2FA" + }, + "description": "Vazio", + "title": "" + }, + "user": { + "data": { + "email": "Endere\u00e7o de e-mail", + "password": "Palavra-passe" + }, + "description": "Vazio", + "title": "Login Google Hangouts" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/es-419.json b/homeassistant/components/homematicip_cloud/.translations/es-419.json index 9af47289380..e15d0dbae64 100644 --- a/homeassistant/components/homematicip_cloud/.translations/es-419.json +++ b/homeassistant/components/homematicip_cloud/.translations/es-419.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Accesspoint ya est\u00e1 configurado", "conection_aborted": "No se pudo conectar al servidor HMIP", + "connection_aborted": "No se pudo conectar al servidor HMIP", "unknown": "Se produjo un error desconocido." }, "error": { @@ -18,6 +19,7 @@ "pin": "C\u00f3digo PIN (opcional)" } } - } + }, + "title": "HomematicIP Cloud" } } \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/fr.json b/homeassistant/components/homematicip_cloud/.translations/fr.json new file mode 100644 index 00000000000..c10cb519133 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/.translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "unknown": "Une erreur inconnue s'est produite" + }, + "error": { + "invalid_pin": "Code PIN invalide, veuillez r\u00e9essayer.", + "press_the_button": "Veuillez appuyer sur le bouton bleu.", + "register_failed": "\u00c9chec d'enregistrement. Veuillez r\u00e9essayer." + }, + "step": { + "init": { + "data": { + "hapid": "ID du point d'acc\u00e8s (SGTIN)", + "name": "Nom (facultatif, utilis\u00e9 comme pr\u00e9fixe de nom pour tous les p\u00e9riph\u00e9riques)", + "pin": "Code PIN (facultatif)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/it.json b/homeassistant/components/homematicip_cloud/.translations/it.json index 2566eb25570..95e600e6d03 100644 --- a/homeassistant/components/homematicip_cloud/.translations/it.json +++ b/homeassistant/components/homematicip_cloud/.translations/it.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "Il punto di accesso \u00e8 gi\u00e0 configurato", + "connection_aborted": "Impossibile connettersi al server HMIP" + }, + "error": { + "press_the_button": "Si prega di premere il pulsante blu.", + "register_failed": "Registrazione fallita, si prega di riprovare." + }, "step": { "init": { "data": { diff --git a/homeassistant/components/homematicip_cloud/.translations/nl.json b/homeassistant/components/homematicip_cloud/.translations/nl.json index 23305a7e584..40d1ced5007 100644 --- a/homeassistant/components/homematicip_cloud/.translations/nl.json +++ b/homeassistant/components/homematicip_cloud/.translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Accesspoint is reeds geconfigureerd", + "already_configured": "Accesspoint is al geconfigureerd", "conection_aborted": "Kon geen verbinding maken met de HMIP-server", "connection_aborted": "Kon geen verbinding maken met de HMIP-server", "unknown": "Er is een onbekende fout opgetreden." @@ -19,11 +19,11 @@ "name": "Naam (optioneel, gebruikt als naamprefix voor alle apparaten)", "pin": "Pin-Code (optioneel)" }, - "title": "Kies HomematicIP Accesspoint" + "title": "Kies HomematicIP accesspoint" }, "link": { - "description": "Druk op de blauwe knop op de accesspoint en de verzendknop om HomematicIP met de Home Assistant te registreren. \n\n![Locatie van knop op brug](/static/images/config_flows/\nconfig_homematicip_cloud.png)", - "title": "Link Accesspoint" + "description": "Druk op de blauwe knop op het accesspoint en de verzendknop om HomematicIP bij Home Assistant te registreren. \n\n![Locatie van knop op bridge](/static/images/config_flows/\nconfig_homematicip_cloud.png)", + "title": "Link accesspoint" } }, "title": "HomematicIP Cloud" diff --git a/homeassistant/components/homematicip_cloud/.translations/pt.json b/homeassistant/components/homematicip_cloud/.translations/pt.json index 2266e83ac44..87ee494a875 100644 --- a/homeassistant/components/homematicip_cloud/.translations/pt.json +++ b/homeassistant/components/homematicip_cloud/.translations/pt.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "O ponto de acesso j\u00e1 se encontra configurado", "conection_aborted": "N\u00e3o foi poss\u00edvel ligar ao servidor HMIP", + "connection_aborted": "N\u00e3o foi poss\u00edvel ligar ao servidor HMIP", "unknown": "Ocorreu um erro desconhecido." }, "error": { diff --git a/homeassistant/components/hue/.translations/nl.json b/homeassistant/components/hue/.translations/nl.json index 88c611b1633..bd065bb7506 100644 --- a/homeassistant/components/hue/.translations/nl.json +++ b/homeassistant/components/hue/.translations/nl.json @@ -24,6 +24,6 @@ "title": "Link Hub" } }, - "title": "Philips Hue Bridge" + "title": "Philips Hue" } } \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/fr.json b/homeassistant/components/nest/.translations/fr.json new file mode 100644 index 00000000000..62a4d7deec9 --- /dev/null +++ b/homeassistant/components/nest/.translations/fr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_setup": "Vous ne pouvez configurer qu'un seul compte Nest." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.pt-BR.json b/homeassistant/components/sensor/.translations/moon.pt-BR.json index af4cefff6e5..57d3a3e95e4 100644 --- a/homeassistant/components/sensor/.translations/moon.pt-BR.json +++ b/homeassistant/components/sensor/.translations/moon.pt-BR.json @@ -1,8 +1,6 @@ { "state": { - "first_quarter": "Quarto crescente", "full_moon": "Cheia", - "last_quarter": "Quarto minguante", "new_moon": "Nova", "waning_crescent": "Minguante", "waning_gibbous": "Minguante gibosa", diff --git a/homeassistant/components/sensor/.translations/moon.pt.json b/homeassistant/components/sensor/.translations/moon.pt.json new file mode 100644 index 00000000000..14961ab98f0 --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.pt.json @@ -0,0 +1,12 @@ +{ + "state": { + "first_quarter": "Quarto crescente", + "full_moon": "Lua cheia", + "last_quarter": "Quarto minguante", + "new_moon": "Lua nova", + "waning_crescent": "Lua crescente", + "waning_gibbous": "Minguante convexa", + "waxing_crescent": "Lua minguante", + "waxing_gibbous": "Crescente convexa" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/de.json b/homeassistant/components/sonos/.translations/de.json index d0587036d24..dd44fca5888 100644 --- a/homeassistant/components/sonos/.translations/de.json +++ b/homeassistant/components/sonos/.translations/de.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "M\u00f6chten Sie Sonos konfigurieren?", + "description": "M\u00f6chten Sie Sonos einrichten?", "title": "Sonos" } }, From 7d852a985cf78ccd3c4b72fb8fc26970197c6802 Mon Sep 17 00:00:00 2001 From: thomaslian Date: Fri, 31 Aug 2018 16:47:10 +0200 Subject: [PATCH 077/172] Upgrade Adafruit-DHT to 1.3.4 (#16327) * Update dht.py * Update requirements_all.txt --- homeassistant/components/sensor/dht.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/dht.py b/homeassistant/components/sensor/dht.py index 0aae10fde64..387a555219d 100644 --- a/homeassistant/components/sensor/dht.py +++ b/homeassistant/components/sensor/dht.py @@ -17,7 +17,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from homeassistant.util.temperature import celsius_to_fahrenheit -REQUIREMENTS = ['Adafruit-DHT==1.3.3'] +REQUIREMENTS = ['Adafruit-DHT==1.3.4'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 0d0acdda32f..7137c4b15d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -18,7 +18,7 @@ voluptuous==0.11.5 --only-binary=all nuimo==0.1.0 # homeassistant.components.sensor.dht -# Adafruit-DHT==1.3.3 +# Adafruit-DHT==1.3.4 # homeassistant.components.sensor.sht31 Adafruit-GPIO==1.0.3 From fa81385b5cdbc409b683ea24406fcc22949f0f64 Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Fri, 31 Aug 2018 10:47:37 -0400 Subject: [PATCH 078/172] Add unique ID (#16323) --- homeassistant/components/cover/myq.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/cover/myq.py b/homeassistant/components/cover/myq.py index 6a17345188a..413794505db 100644 --- a/homeassistant/components/cover/myq.py +++ b/homeassistant/components/cover/myq.py @@ -120,6 +120,11 @@ class MyQDevice(CoverDevice): """Flag supported features.""" return SUPPORT_OPEN | SUPPORT_CLOSE + @property + def unique_id(self): + """Return a unique, HASS-friendly identifier for this entity.""" + return self.device_id + def update(self): """Update status of cover.""" self._status = self.myq.get_status(self.device_id) From d3791fa45db0d56aec53424931d9f04aaa68b12a Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Fri, 31 Aug 2018 17:56:26 -0400 Subject: [PATCH 079/172] Add Cover to the Insteon component (#16215) * Create cover platform * Create insteon cover platform * Bump insteonplm to 0.13.0 * Change async_add_devices to async_add_entities * Missing doc string * Simplify open and set_position * Flake8 * Bump insteonplm to 0.13.1 * Code review changes * Flake8 updates --- homeassistant/components/cover/insteon.py | 73 ++++++++++++++++++++ homeassistant/components/insteon/__init__.py | 15 ++-- requirements_all.txt | 2 +- 3 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/cover/insteon.py diff --git a/homeassistant/components/cover/insteon.py b/homeassistant/components/cover/insteon.py new file mode 100644 index 00000000000..f0cf93c13e9 --- /dev/null +++ b/homeassistant/components/cover/insteon.py @@ -0,0 +1,73 @@ +""" +Support for Insteon covers via PowerLinc Modem. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/cover.insteon/ +""" +import logging +import math + +from homeassistant.components.insteon import InsteonEntity +from homeassistant.components.cover import (CoverDevice, ATTR_POSITION, + SUPPORT_OPEN, SUPPORT_CLOSE, + SUPPORT_SET_POSITION) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['insteon'] +SUPPORTED_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the Insteon platform.""" + if not discovery_info: + return + + insteon_modem = hass.data['insteon'].get('modem') + + address = discovery_info['address'] + device = insteon_modem.devices[address] + state_key = discovery_info['state_key'] + + _LOGGER.debug('Adding device %s entity %s to Cover platform', + device.address.hex, device.states[state_key].name) + + new_entity = InsteonCoverDevice(device, state_key) + + async_add_entities([new_entity]) + + +class InsteonCoverDevice(InsteonEntity, CoverDevice): + """A Class for an Insteon device.""" + + @property + def current_cover_position(self): + """Return the current cover position.""" + return int(math.ceil(self._insteon_device_state.value*100/255)) + + @property + def supported_features(self): + """Return the supported features for this entity.""" + return SUPPORTED_FEATURES + + @property + def is_closed(self): + """Return the boolean response if the node is on.""" + return bool(self.current_cover_position) + + async def async_open_cover(self, **kwargs): + """Open device.""" + self._insteon_device_state.open() + + async def async_close_cover(self, **kwargs): + """Close device.""" + self._insteon_device_state.close() + + async def async_set_cover_position(self, **kwargs): + """Set the cover position.""" + position = int(kwargs[ATTR_POSITION]*255/100) + if position == 0: + self._insteon_device_state.close() + else: + self._insteon_device_state.set_position(position) diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index 212cdbac3b8..d79640b77ab 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -18,7 +18,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['insteonplm==0.12.3'] +REQUIREMENTS = ['insteonplm==0.13.1'] _LOGGER = logging.getLogger(__name__) @@ -358,6 +358,8 @@ class IPDB: def __init__(self): """Create the INSTEON Product Database (IPDB).""" + from insteonplm.states.cover import Cover + from insteonplm.states.onOff import (OnOffSwitch, OnOffSwitch_OutletTop, OnOffSwitch_OutletBottom, @@ -383,7 +385,9 @@ class IPDB: X10AllLightsOnSensor, X10AllLightsOffSensor) - self.states = [State(OnOffSwitch_OutletTop, 'switch'), + self.states = [State(Cover, 'cover'), + + State(OnOffSwitch_OutletTop, 'switch'), State(OnOffSwitch_OutletBottom, 'switch'), State(OpenClosedRelay, 'switch'), State(OnOffSwitch, 'switch'), @@ -470,11 +474,10 @@ class InsteonEntity(Entity): return attributes @callback - def async_entity_update(self, deviceid, statename, val): + def async_entity_update(self, deviceid, group, val): """Receive notification from transport that new data exists.""" - _LOGGER.debug('Received update for device %s group %d statename %s', - self.address, self.group, - self._insteon_device_state.name) + _LOGGER.debug('Received update for device %s group %d value %s', + deviceid.human, group, val) self.async_schedule_update_ha_state() @asyncio.coroutine diff --git a/requirements_all.txt b/requirements_all.txt index 7137c4b15d2..ff95a8fc2ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -482,7 +482,7 @@ ihcsdk==2.2.0 influxdb==5.0.0 # homeassistant.components.insteon -insteonplm==0.12.3 +insteonplm==0.13.1 # homeassistant.components.sensor.iperf3 iperf3==0.1.10 From 2c7d6ee6b538de16e0de4c93b0667faf4d934a33 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Sat, 1 Sep 2018 10:40:16 +0200 Subject: [PATCH 080/172] Fix missing humidity sensor (#16337) --- homeassistant/components/homematic/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 53c8e267016..2b517652ad7 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -77,7 +77,7 @@ HM_DEVICE_TYPES = { 'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall', 'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat', 'IPWeatherSensor', 'RotaryHandleSensorIP', 'IPPassageSensor', - 'IPKeySwitchPowermeter'], + 'IPKeySwitchPowermeter', 'IPThermostatWall230V'], DISCOVER_CLIMATE: [ 'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2', 'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall', From 901cfef78e5961cf88055b2d01be39773df567d2 Mon Sep 17 00:00:00 2001 From: Philipp Temminghoff Date: Sat, 1 Sep 2018 16:02:38 +0200 Subject: [PATCH 081/172] Support Sonos Beam HDMI input (#16340) --- homeassistant/components/media_player/sonos.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 4fc6b8b0954..456252aded4 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -884,6 +884,8 @@ class SonosDevice(MediaPlayerDevice): sources += [SOURCE_LINEIN] elif 'PLAYBAR' in model: sources += [SOURCE_LINEIN, SOURCE_TV] + elif 'BEAM' in model: + sources += [SOURCE_TV] return sources From a5d95dfbdcf0d9ca3da93f44e2cbb5d162ac3272 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Sat, 1 Sep 2018 11:49:03 -0500 Subject: [PATCH 082/172] Make last_seen attribute a timezone aware datetime in UTC (#16348) The last_seen attribute was a datetime in the local timezone but with no tzinfo (i.e., a "naive" datetime.) When state changes occurred it would be printed incorrectly in homeassistant.log because homeassistant.util.dt.as_local assumes any datetime without tzinfo is UTC. Also most, if not all, datetime attributes are timezone aware in UTC. So use homeassistant.util.dt.as_utc (which assumes a naive datetime is local) to convert last_seen to a timezone aware datetime in UTC. --- homeassistant/components/device_tracker/google_maps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py index 8c21e71bd30..170d3de6800 100644 --- a/homeassistant/components/device_tracker/google_maps.py +++ b/homeassistant/components/device_tracker/google_maps.py @@ -15,7 +15,7 @@ from homeassistant.const import ATTR_ID, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.typing import ConfigType -from homeassistant.util import slugify +from homeassistant.util import slugify, dt as dt_util REQUIREMENTS = ['locationsharinglib==2.0.11'] @@ -92,7 +92,7 @@ class GoogleMapsScanner: ATTR_ADDRESS: person.address, ATTR_FULL_NAME: person.full_name, ATTR_ID: person.id, - ATTR_LAST_SEEN: person.datetime, + ATTR_LAST_SEEN: dt_util.as_utc(person.datetime), ATTR_NICKNAME: person.nickname, } self.see( From b31890c4cbc18ec98ae92e3caefa5f88f6532367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sat, 1 Sep 2018 23:30:34 +0200 Subject: [PATCH 083/172] Handle netatmo exception (#16344) --- homeassistant/components/sensor/netatmo.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/netatmo.py b/homeassistant/components/sensor/netatmo.py index 5216913528a..f709e0169cf 100644 --- a/homeassistant/components/sensor/netatmo.py +++ b/homeassistant/components/sensor/netatmo.py @@ -317,7 +317,11 @@ class NetAtmoData: try: import pyatmo - self.station_data = pyatmo.WeatherStationData(self.auth) + try: + self.station_data = pyatmo.WeatherStationData(self.auth) + except TypeError: + _LOGGER.error("Failed to connect to NetAtmo") + return # finally statement will be executed if self.station is not None: self.data = self.station_data.lastData( From 444df5b09a9a853beb20b25fae6c3e7b7349f786 Mon Sep 17 00:00:00 2001 From: Joshi <42069141+Joshi425@users.noreply.github.com> Date: Sat, 1 Sep 2018 23:34:38 +0200 Subject: [PATCH 084/172] Add support for sound_mode for Yamaha rxv media_player (#16352) --- .../components/media_player/yamaha.py | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py index 2ffe58b02af..be6df547f1d 100644 --- a/homeassistant/components/media_player/yamaha.py +++ b/homeassistant/components/media_player/yamaha.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - MediaPlayerDevice) + SUPPORT_SELECT_SOUND_MODE, MediaPlayerDevice) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, STATE_IDLE, STATE_OFF, STATE_ON, STATE_PLAYING) @@ -43,7 +43,8 @@ ENABLE_OUTPUT_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ SERVICE_ENABLE_OUTPUT = 'yamaha_enable_output' SUPPORT_YAMAHA = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ - SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE | SUPPORT_PLAY + SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE | SUPPORT_PLAY \ + | SUPPORT_SELECT_SOUND_MODE PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -140,6 +141,8 @@ class YamahaDevice(MediaPlayerDevice): self._volume = 0 self._pwstate = STATE_OFF self._current_source = None + self._sound_mode = None + self._sound_mode_list = None self._source_list = None self._source_ignore = source_ignore or [] self._source_names = source_names or {} @@ -181,6 +184,8 @@ class YamahaDevice(MediaPlayerDevice): self._playback_support = self.receiver.get_playback_support() self._is_playback_supported = self.receiver.is_playback_supported( self._current_source) + self._sound_mode = self.receiver.surround_program + self._sound_mode_list = self.receiver.surround_programs() def build_source_list(self): """Build the source list.""" @@ -222,6 +227,16 @@ class YamahaDevice(MediaPlayerDevice): """Return the current input source.""" return self._current_source + @property + def sound_mode(self): + """Return the current sound mode.""" + return self._sound_mode + + @property + def sound_mode_list(self): + """Return the current sound mode.""" + return self._sound_mode_list + @property def source_list(self): """List of available input sources.""" @@ -330,6 +345,10 @@ class YamahaDevice(MediaPlayerDevice): """Enable or disable an output port..""" self.receiver.enable_output(port, enabled) + def select_sound_mode(self, sound_mode): + """Set Sound Mode for Receiver..""" + self.receiver.surround_program = sound_mode + @property def media_artist(self): """Artist of current playing media.""" From e75a1690d14ac23ae40db11a4cfb6afe085f4ab2 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 1 Sep 2018 23:37:03 +0200 Subject: [PATCH 085/172] Add unique_id to MQTT Light (#16303) * Add unique_id * Delete whitespaces --- homeassistant/components/light/mqtt.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index 225f0f510ad..64331411f7f 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -54,6 +54,7 @@ CONF_WHITE_VALUE_SCALE = 'white_value_scale' CONF_WHITE_VALUE_STATE_TOPIC = 'white_value_state_topic' CONF_WHITE_VALUE_TEMPLATE = 'white_value_template' CONF_ON_COMMAND_TYPE = 'on_command_type' +CONF_UNIQUE_ID = 'unique_id' DEFAULT_BRIGHTNESS_SCALE = 255 DEFAULT_NAME = 'MQTT Light' @@ -79,6 +80,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_EFFECT_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_EFFECT_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, @@ -111,6 +113,7 @@ async def async_setup_platform(hass, config, async_add_entities, async_add_entities([MqttLight( config.get(CONF_NAME), + config.get(CONF_UNIQUE_ID), config.get(CONF_EFFECT_LIST), { key: config.get(key) for key in ( @@ -159,14 +162,15 @@ async def async_setup_platform(hass, config, async_add_entities, class MqttLight(MqttAvailability, Light): """Representation of a MQTT light.""" - def __init__(self, name, effect_list, topic, templates, qos, - retain, payload, optimistic, brightness_scale, + def __init__(self, name, unique_id, effect_list, topic, templates, + qos, retain, payload, optimistic, brightness_scale, white_value_scale, on_command_type, availability_topic, payload_available, payload_not_available): """Initialize MQTT light.""" super().__init__(availability_topic, qos, payload_available, payload_not_available) self._name = name + self._unique_id = unique_id self._effect_list = effect_list self._topic = topic self._qos = qos @@ -392,6 +396,11 @@ class MqttLight(MqttAvailability, Light): """Return the name of the device if any.""" return self._name + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id + @property def is_on(self): """Return true if device is on.""" From 2b0b431a2a5a6e1c0aa7862ee6e84c684b4db18b Mon Sep 17 00:00:00 2001 From: Jesse Rizzo <32472573+jesserizzo@users.noreply.github.com> Date: Sat, 1 Sep 2018 16:45:47 -0500 Subject: [PATCH 086/172] Update to EnvoyReader 0.2, support for more hardware (#16212) * Add support for older Envoy models * Stop requiring envoy model name in config * Update to envoy_reader0.2 * Minor formatting fixes * run script/gen_requirements_all.py * Minor formatting fixes * Change some strings to constants, use getattr to call function --- .../components/sensor/enphase_envoy.py | 35 +++++-------------- requirements_all.txt | 2 +- 2 files changed, 9 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/sensor/enphase_envoy.py b/homeassistant/components/sensor/enphase_envoy.py index 6afe887537c..7f8cff0f885 100644 --- a/homeassistant/components/sensor/enphase_envoy.py +++ b/homeassistant/components/sensor/enphase_envoy.py @@ -14,25 +14,27 @@ import homeassistant.helpers.config_validation as cv from homeassistant.const import (CONF_IP_ADDRESS, CONF_MONITORED_CONDITIONS) -REQUIREMENTS = ['envoy_reader==0.1'] +REQUIREMENTS = ['envoy_reader==0.2'] _LOGGER = logging.getLogger(__name__) SENSORS = { "production": ("Envoy Current Energy Production", 'W'), "daily_production": ("Envoy Today's Energy Production", "Wh"), - "7_days_production": ("Envoy Last Seven Days Energy Production", "Wh"), + "seven_days_production": ("Envoy Last Seven Days Energy Production", "Wh"), "lifetime_production": ("Envoy Lifetime Energy Production", "Wh"), "consumption": ("Envoy Current Energy Consumption", "W"), "daily_consumption": ("Envoy Today's Energy Consumption", "Wh"), - "7_days_consumption": ("Envoy Last Seven Days Energy Consumption", "Wh"), + "seven_days_consumption": ("Envoy Last Seven Days Energy Consumption", + "Wh"), "lifetime_consumption": ("Envoy Lifetime Energy Consumption", "Wh") } ICON = 'mdi:flash' +CONST_DEFAULT_HOST = "envoy" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Optional(CONF_IP_ADDRESS, default=CONST_DEFAULT_HOST): cv.string, vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): vol.All(cv.ensure_list, [vol.In(list(SENSORS))])}) @@ -81,27 +83,6 @@ class Envoy(Entity): def update(self): """Get the energy production data from the Enphase Envoy.""" - import envoy_reader + from envoy_reader import EnvoyReader - if self._type == "production": - self._state = int(envoy_reader.production(self._ip_address)) - elif self._type == "daily_production": - self._state = int(envoy_reader.daily_production(self._ip_address)) - elif self._type == "7_days_production": - self._state = int(envoy_reader.seven_days_production( - self._ip_address)) - elif self._type == "lifetime_production": - self._state = int(envoy_reader.lifetime_production( - self._ip_address)) - - elif self._type == "consumption": - self._state = int(envoy_reader.consumption(self._ip_address)) - elif self._type == "daily_consumption": - self._state = int(envoy_reader.daily_consumption( - self._ip_address)) - elif self._type == "7_days_consumption": - self._state = int(envoy_reader.seven_days_consumption( - self._ip_address)) - elif self._type == "lifetime_consumption": - self._state = int(envoy_reader.lifetime_consumption( - self._ip_address)) + self._state = getattr(EnvoyReader(self._ip_address), self._type)() diff --git a/requirements_all.txt b/requirements_all.txt index ff95a8fc2ad..8c2080d12fa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -321,7 +321,7 @@ enocean==0.40 # envirophat==0.0.6 # homeassistant.components.sensor.enphase_envoy -envoy_reader==0.1 +envoy_reader==0.2 # homeassistant.components.sensor.season ephem==3.7.6.0 From 3797b6b012b79e384e20fecd9786a539b6941b22 Mon Sep 17 00:00:00 2001 From: Tod Schmidt Date: Sat, 1 Sep 2018 18:01:11 -0400 Subject: [PATCH 087/172] Snips: Added special slot values, session_id and slotname_raw (#16185) * Added special slot values, site_id, session_id, and slotname_raw * Update snips.py --- homeassistant/components/snips.py | 3 +++ tests/components/test_snips.py | 17 +++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/snips.py b/homeassistant/components/snips.py index 34290819106..88a93408056 100644 --- a/homeassistant/components/snips.py +++ b/homeassistant/components/snips.py @@ -131,7 +131,10 @@ async def async_setup(hass, config): slots = {} for slot in request.get('slots', []): slots[slot['slotName']] = {'value': resolve_slot_values(slot)} + slots['{}_raw'.format(slot['slotName'])] = { + 'value': slot['rawValue']} slots['site_id'] = {'value': request.get('siteId')} + slots['session_id'] = {'value': request.get('sessionId')} slots['probability'] = {'value': request['intent']['probability']} try: diff --git a/tests/components/test_snips.py b/tests/components/test_snips.py index baeda2c49a8..bc044999bdd 100644 --- a/tests/components/test_snips.py +++ b/tests/components/test_snips.py @@ -93,6 +93,8 @@ async def test_snips_intent(hass, mqtt_mock): assert result payload = """ { + "siteId": "default", + "sessionId": "1234567890ABCDEF", "input": "turn the lights green", "intent": { "intentName": "Lights", @@ -104,7 +106,8 @@ async def test_snips_intent(hass, mqtt_mock): "value": { "kind": "Custom", "value": "green" - } + }, + "rawValue": "green" } ] } @@ -119,9 +122,12 @@ async def test_snips_intent(hass, mqtt_mock): intent = intents[0] assert intent.platform == 'snips' assert intent.intent_type == 'Lights' + assert intent assert intent.slots == {'light_color': {'value': 'green'}, + 'light_color_raw': {'value': 'green'}, 'probability': {'value': 1}, - 'site_id': {'value': None}} + 'site_id': {'value': 'default'}, + 'session_id': {'value': '1234567890ABCDEF'}} assert intent.text_input == 'turn the lights green' @@ -147,7 +153,8 @@ async def test_snips_service_intent(hass, mqtt_mock): "value": { "kind": "Custom", "value": "kitchen" - } + }, + "rawValue": "green" } ] } @@ -217,7 +224,9 @@ async def test_snips_intent_with_duration(hass, mqtt_mock): assert intent.intent_type == 'SetTimer' assert intent.slots == {'probability': {'value': 1}, 'site_id': {'value': None}, - 'timer_duration': {'value': 300}} + 'session_id': {'value': None}, + 'timer_duration': {'value': 300}, + 'timer_duration_raw': {'value': 'five minutes'}} async def test_intent_speech_response(hass, mqtt_mock): From 87eb6cd25a9bd100f707b9e2970cdd325cbef68d Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Sun, 2 Sep 2018 03:50:30 -0700 Subject: [PATCH 088/172] Upgrade hbmqtt to 0.9.4 (#16356) * Upgrade to hbmqtt 0.9.4 * Lint * Typo --- homeassistant/components/mqtt/server.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/mqtt/test_server.py | 6 ------ 4 files changed, 3 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mqtt/server.py b/homeassistant/components/mqtt/server.py index 5fc365342ae..57da85fe5f6 100644 --- a/homeassistant/components/mqtt/server.py +++ b/homeassistant/components/mqtt/server.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.const import EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['hbmqtt==0.9.2'] +REQUIREMENTS = ['hbmqtt==0.9.4'] DEPENDENCIES = ['http'] # None allows custom config to be created through generate_config diff --git a/requirements_all.txt b/requirements_all.txt index 8c2080d12fa..db244cc8cfb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -430,7 +430,7 @@ hangups==0.4.5 haversine==0.4.5 # homeassistant.components.mqtt.server -hbmqtt==0.9.2 +hbmqtt==0.9.4 # homeassistant.components.climate.heatmiser heatmiserV3==0.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bd921085461..446e3d056a3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -78,7 +78,7 @@ hangups==0.4.5 haversine==0.4.5 # homeassistant.components.mqtt.server -hbmqtt==0.9.2 +hbmqtt==0.9.4 # homeassistant.components.binary_sensor.workday holidays==0.9.6 diff --git a/tests/components/mqtt/test_server.py b/tests/components/mqtt/test_server.py index c761c47542f..976fdd3d15c 100644 --- a/tests/components/mqtt/test_server.py +++ b/tests/components/mqtt/test_server.py @@ -1,8 +1,5 @@ """The tests for the MQTT component embedded server.""" from unittest.mock import Mock, MagicMock, patch -import sys - -import pytest from homeassistant.const import CONF_PASSWORD from homeassistant.setup import setup_component @@ -11,9 +8,6 @@ import homeassistant.components.mqtt as mqtt from tests.common import get_test_home_assistant, mock_coro -# Until https://github.com/beerfactory/hbmqtt/pull/139 is released -@pytest.mark.skipif(sys.version_info[:2] >= (3, 7), - reason='Package incompatible with Python 3.7') class TestMQTT: """Test the MQTT component.""" From 03fb2b32a6a8ef6becbab41237bf6720ae94838d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 2 Sep 2018 12:51:07 +0200 Subject: [PATCH 089/172] Upgrade Sphinx to 1.7.7 (#16359) --- requirements_docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_docs.txt b/requirements_docs.txt index a7436cad2fc..e26c97887b7 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,3 +1,3 @@ -Sphinx==1.7.6 +Sphinx==1.7.7 sphinx-autodoc-typehints==1.3.0 sphinx-autodoc-annotation==1.0.post1 From 15ad82b9bdc384beb3fa8b31c9c39ad9372ec865 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 2 Sep 2018 12:51:25 +0200 Subject: [PATCH 090/172] Upgrade qnapstats to 0.2.7 (#16360) --- homeassistant/components/sensor/qnap.py | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/qnap.py b/homeassistant/components/sensor/qnap.py index 22f8d4c37c0..29eb8cd6749 100644 --- a/homeassistant/components/sensor/qnap.py +++ b/homeassistant/components/sensor/qnap.py @@ -17,7 +17,7 @@ from homeassistant.const import ( from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['qnapstats==0.2.6'] +REQUIREMENTS = ['qnapstats==0.2.7'] _LOGGER = logging.getLogger(__name__) @@ -173,7 +173,7 @@ class QNAPStatsAPI: protocol = "https" if config.get(CONF_SSL) else "http" self._api = QNAPStats( - protocol + "://" + config.get(CONF_HOST), + '{}://{}'.format(protocol, config.get(CONF_HOST)), config.get(CONF_PORT), config.get(CONF_USERNAME), config.get(CONF_PASSWORD), diff --git a/requirements_all.txt b/requirements_all.txt index db244cc8cfb..49fc3b42466 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1217,7 +1217,7 @@ pyxeoma==1.4.0 pyzabbix==0.7.4 # homeassistant.components.sensor.qnap -qnapstats==0.2.6 +qnapstats==0.2.7 # homeassistant.components.rachio rachiopy==0.1.3 From 1d12c7b0e7d30348e9f5f5cf7e52eecffba91a6d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 2 Sep 2018 12:51:36 +0200 Subject: [PATCH 091/172] Upgrade mutagen to 1.41.1 (#16361) --- homeassistant/components/tts/__init__.py | 12 ++++++------ requirements_all.txt | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index f060c9f353a..2ec9a2ab801 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -29,7 +29,7 @@ from homeassistant.helpers import config_per_platform import homeassistant.helpers.config_validation as cv from homeassistant.setup import async_prepare_setup_platform -REQUIREMENTS = ['mutagen==1.41.0'] +REQUIREMENTS = ['mutagen==1.41.1'] _LOGGER = logging.getLogger(__name__) @@ -69,8 +69,8 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ SCHEMA_SERVICE_SAY = vol.Schema({ vol.Required(ATTR_MESSAGE): cv.string, - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_CACHE): cv.boolean, + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_LANGUAGE): cv.string, vol.Optional(ATTR_OPTIONS): dict, }) @@ -117,7 +117,7 @@ async def async_setup(hass, config): tts.async_register_engine(p_type, provider, p_config) except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error setting up platform %s", p_type) + _LOGGER.exception("Error setting up platform: %s", p_type) return async def async_say_handle(service): @@ -134,7 +134,7 @@ async def async_setup(hass, config): options=options ) except HomeAssistantError as err: - _LOGGER.error("Error on init tts: %s", err) + _LOGGER.error("Error on init TTS: %s", err) return data = { @@ -302,8 +302,8 @@ class SpeechManager: return "{}/api/tts_proxy/{}".format( self.hass.config.api.base_url, filename) - async def async_get_tts_audio(self, engine, key, message, cache, language, - options): + async def async_get_tts_audio( + self, engine, key, message, cache, language, options): """Receive TTS and store for view in cache. This method is a coroutine. diff --git a/requirements_all.txt b/requirements_all.txt index 49fc3b42466..64a9d6bc133 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -585,7 +585,7 @@ mitemp_bt==0.0.1 motorparts==1.0.2 # homeassistant.components.tts -mutagen==1.41.0 +mutagen==1.41.1 # homeassistant.components.mychevy mychevy==0.4.0 From 97695a30f5f9af6ffd4a399d404290a97fb428ae Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 2 Sep 2018 12:51:56 +0200 Subject: [PATCH 092/172] Upgrade shodan to 1.10.0 (#16363) --- homeassistant/components/sensor/shodan.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/shodan.py b/homeassistant/components/sensor/shodan.py index fd462d6811c..cfafcb67608 100644 --- a/homeassistant/components/sensor/shodan.py +++ b/homeassistant/components/sensor/shodan.py @@ -14,7 +14,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['shodan==1.9.1'] +REQUIREMENTS = ['shodan==1.10.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 64a9d6bc133..1781a70c6ca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1290,7 +1290,7 @@ sense_energy==0.4.1 sharp_aquos_rc==0.3.2 # homeassistant.components.sensor.shodan -shodan==1.9.1 +shodan==1.10.0 # homeassistant.components.notify.simplepush simplepush==1.1.4 From 52e922171daa5ea9ff7f33651d6c1b650c918b61 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 2 Sep 2018 14:33:20 +0200 Subject: [PATCH 093/172] Upgrade to youtube_dl to 2018.09.01 (#16365) --- homeassistant/components/media_extractor.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py index 8f2abb9be19..f6e982a77ef 100644 --- a/homeassistant/components/media_extractor.py +++ b/homeassistant/components/media_extractor.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['youtube_dl==2018.08.22'] +REQUIREMENTS = ['youtube_dl==2018.09.01'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 1781a70c6ca..b2ac021d61e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1512,7 +1512,7 @@ yeelight==0.4.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2018.08.22 +youtube_dl==2018.09.01 # homeassistant.components.light.zengge zengge==0.2 From 357e5eadb87a5fd8159c98ea17f1335ed5cf374a Mon Sep 17 00:00:00 2001 From: MarcSN311 Date: Sun, 2 Sep 2018 15:51:15 +0200 Subject: [PATCH 094/172] Added 'nomapnt', 'outcurnt', 'loadapnt' fields (#16176) * Added 'nomapnt', 'outcurnt', 'loadapnt' fields Also added Ampere and Volt-Ampere to INFERRED_UNITS * Fix lint issue --- homeassistant/components/sensor/apcupsd.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/sensor/apcupsd.py b/homeassistant/components/sensor/apcupsd.py index 897f0626b7c..90c1f2e6795 100644 --- a/homeassistant/components/sensor/apcupsd.py +++ b/homeassistant/components/sensor/apcupsd.py @@ -49,6 +49,7 @@ SENSOR_TYPES = { 'linefreq': ['Line Frequency', 'Hz', 'mdi:information-outline'], 'linev': ['Input Voltage', 'V', 'mdi:flash'], 'loadpct': ['Load', '%', 'mdi:gauge'], + 'loadapnt': ['Load Apparent Power', '%', 'mdi:gauge'], 'lotrans': ['Transfer Low', 'V', 'mdi:flash'], 'mandate': ['Manufacture Date', '', 'mdi:calendar'], 'masterupd': ['Master Update', '', 'mdi:information-outline'], @@ -62,7 +63,9 @@ SENSOR_TYPES = { 'nominv': ['Nominal Input Voltage', 'V', 'mdi:flash'], 'nomoutv': ['Nominal Output Voltage', 'V', 'mdi:flash'], 'nompower': ['Nominal Output Power', 'W', 'mdi:flash'], + 'nomapnt': ['Nominal Apparent Power', 'VA', 'mdi:flash'], 'numxfers': ['Transfer Count', '', 'mdi:counter'], + 'outcurnt': ['Output Current', 'A', 'mdi:flash'], 'outputv': ['Output Voltage', 'V', 'mdi:flash'], 'reg1': ['Register 1 Fault', '', 'mdi:information-outline'], 'reg2': ['Register 2 Fault', '', 'mdi:information-outline'], @@ -93,6 +96,8 @@ INFERRED_UNITS = { ' Seconds': 'sec', ' Percent': '%', ' Volts': 'V', + ' Ampere': 'A', + ' Volt-Ampere': 'VA', ' Watts': 'W', ' Hz': 'Hz', ' C': TEMP_CELSIUS, From b29c296ced611231ec8000d2cbe8425b4c130eae Mon Sep 17 00:00:00 2001 From: Lev Aronsky Date: Sun, 2 Sep 2018 17:42:08 +0300 Subject: [PATCH 095/172] Generic Thermostat: add support for climate.turn_on/climate.turn_off (#16080) * Added async_turn_on and async_turn_off implementations. * Added turning on/off tests to generic thermostat * style * style * style --- .../components/climate/generic_thermostat.py | 8 ++ .../climate/test_generic_thermostat.py | 85 +++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index fec18329878..85879b8122a 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -251,6 +251,14 @@ class GenericThermostat(ClimateDevice): # Ensure we update the current operation after changing the mode self.schedule_update_ha_state() + async def async_turn_on(self): + """Turn thermostat on.""" + await self.async_set_operation_mode(self.operation_list[0]) + + async def async_turn_off(self): + """Turn thermostat off.""" + await self.async_set_operation_mode(STATE_OFF) + async def async_set_temperature(self, **kwargs): """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) diff --git a/tests/components/climate/test_generic_thermostat.py b/tests/components/climate/test_generic_thermostat.py index ac587db13fa..c4e07705230 100644 --- a/tests/components/climate/test_generic_thermostat.py +++ b/tests/components/climate/test_generic_thermostat.py @@ -21,6 +21,7 @@ from homeassistant import loader from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.util.async_ import run_coroutine_threadsafe from homeassistant.components import climate, input_boolean, switch +from homeassistant.components.climate import STATE_HEAT, STATE_COOL import homeassistant.components as comps from tests.common import (assert_setup_component, get_test_home_assistant, mock_restore_cache) @@ -894,6 +895,90 @@ class TestClimateGenericThermostatKeepAlive(unittest.TestCase): self.hass.services.register(ha.DOMAIN, SERVICE_TURN_OFF, log_call) +class TestClimateGenericThermostatTurnOnOff(unittest.TestCase): + """Test the Generic Thermostat.""" + + HEAT_ENTITY = 'climate.test_heat' + COOL_ENTITY = 'climate.test_cool' + + def setUp(self): # pylint: disable=invalid-name + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + assert setup_component(self.hass, climate.DOMAIN, {'climate': [ + { + 'platform': 'generic_thermostat', + 'name': 'test_heat', + 'heater': ENT_SWITCH, + 'target_sensor': ENT_SENSOR + }, + { + 'platform': 'generic_thermostat', + 'name': 'test_cool', + 'heater': ENT_SWITCH, + 'ac_mode': True, + 'target_sensor': ENT_SENSOR + } + ]}) + + def tearDown(self): # pylint: disable=invalid-name + """Stop down everything that was started.""" + self.hass.stop() + + def test_turn_on_when_off(self): + """Test if climate.turn_on turns on a turned off device.""" + climate.set_operation_mode(self.hass, STATE_OFF) + self.hass.block_till_done() + self.hass.services.call('climate', SERVICE_TURN_ON) + self.hass.block_till_done() + state_heat = self.hass.states.get(self.HEAT_ENTITY) + state_cool = self.hass.states.get(self.COOL_ENTITY) + self.assertEqual(STATE_HEAT, + state_heat.attributes.get('operation_mode')) + self.assertEqual(STATE_COOL, + state_cool.attributes.get('operation_mode')) + + def test_turn_on_when_on(self): + """Test if climate.turn_on does nothing to a turned on device.""" + climate.set_operation_mode(self.hass, STATE_HEAT, self.HEAT_ENTITY) + climate.set_operation_mode(self.hass, STATE_COOL, self.COOL_ENTITY) + self.hass.block_till_done() + self.hass.services.call('climate', SERVICE_TURN_ON) + self.hass.block_till_done() + state_heat = self.hass.states.get(self.HEAT_ENTITY) + state_cool = self.hass.states.get(self.COOL_ENTITY) + self.assertEqual(STATE_HEAT, + state_heat.attributes.get('operation_mode')) + self.assertEqual(STATE_COOL, + state_cool.attributes.get('operation_mode')) + + def test_turn_off_when_on(self): + """Test if climate.turn_off turns off a turned on device.""" + climate.set_operation_mode(self.hass, STATE_HEAT, self.HEAT_ENTITY) + climate.set_operation_mode(self.hass, STATE_COOL, self.COOL_ENTITY) + self.hass.block_till_done() + self.hass.services.call('climate', SERVICE_TURN_OFF) + self.hass.block_till_done() + state_heat = self.hass.states.get(self.HEAT_ENTITY) + state_cool = self.hass.states.get(self.COOL_ENTITY) + self.assertEqual(STATE_OFF, + state_heat.attributes.get('operation_mode')) + self.assertEqual(STATE_OFF, + state_cool.attributes.get('operation_mode')) + + def test_turn_off_when_off(self): + """Test if climate.turn_off does nothing to a turned off device.""" + climate.set_operation_mode(self.hass, STATE_OFF) + self.hass.block_till_done() + self.hass.services.call('climate', SERVICE_TURN_OFF) + self.hass.block_till_done() + state_heat = self.hass.states.get(self.HEAT_ENTITY) + state_cool = self.hass.states.get(self.COOL_ENTITY) + self.assertEqual(STATE_OFF, + state_heat.attributes.get('operation_mode')) + self.assertEqual(STATE_OFF, + state_cool.attributes.get('operation_mode')) + + @asyncio.coroutine def test_custom_setup_params(hass): """Test the setup with custom parameters.""" From a5cff9877e343cdd5754f4e4d6fac77445ed1f68 Mon Sep 17 00:00:00 2001 From: Martin Fuchs <39280548+fucm@users.noreply.github.com> Date: Sun, 2 Sep 2018 17:02:51 +0200 Subject: [PATCH 096/172] Add support for Tahoma Lighting Receiver on/off io (#15925) * Add support for Tahoma light switch * Clean up attributes and add available method * Remove else statement --- homeassistant/components/switch/tahoma.py | 70 ++++++++++++++++++++++- homeassistant/components/tahoma.py | 1 + 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/tahoma.py b/homeassistant/components/switch/tahoma.py index 2904f06b432..bcac038d43b 100644 --- a/homeassistant/components/switch/tahoma.py +++ b/homeassistant/components/switch/tahoma.py @@ -12,11 +12,14 @@ import logging from homeassistant.components.switch import SwitchDevice from homeassistant.components.tahoma import ( DOMAIN as TAHOMA_DOMAIN, TahomaDevice) +from homeassistant.const import (STATE_OFF, STATE_ON) DEPENDENCIES = ['tahoma'] _LOGGER = logging.getLogger(__name__) +ATTR_RSSI_LEVEL = 'rssi_level' + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Tahoma switches.""" @@ -30,6 +33,33 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class TahomaSwitch(TahomaDevice, SwitchDevice): """Representation a Tahoma Switch.""" + def __init__(self, tahoma_device, controller): + """Initialize the switch.""" + super().__init__(tahoma_device, controller) + self._state = STATE_OFF + self._skip_update = False + self._available = False + + def update(self): + """Update method.""" + # Postpone the immediate state check for changes that take time. + if self._skip_update: + self._skip_update = False + return + + self.controller.get_states([self.tahoma_device]) + + if self.tahoma_device.type == 'io:OnOffLightIOComponent': + if self.tahoma_device.active_states.get('core:OnOffState') == 'on': + self._state = STATE_ON + else: + self._state = STATE_OFF + + self._available = bool(self.tahoma_device.active_states.get( + 'core:StatusState') == 'available') + + _LOGGER.debug("Update %s, state: %s", self._name, self._state) + @property def device_class(self): """Return the class of the device.""" @@ -39,7 +69,23 @@ class TahomaSwitch(TahomaDevice, SwitchDevice): def turn_on(self, **kwargs): """Send the on command.""" - self.toggle() + _LOGGER.debug("Turn on: %s", self._name) + if self.tahoma_device.type == 'rts:GarageDoor4TRTSComponent': + self.toggle() + else: + self.apply_action('on') + self._skip_update = True + self._state = STATE_ON + + def turn_off(self, **kwargs): + """Send the off command.""" + _LOGGER.debug("Turn off: %s", self._name) + if self.tahoma_device.type == 'rts:GarageDoor4TRTSComponent': + return + + self.apply_action('off') + self._skip_update = True + self._state = STATE_OFF def toggle(self, **kwargs): """Click the switch.""" @@ -48,4 +94,24 @@ class TahomaSwitch(TahomaDevice, SwitchDevice): @property def is_on(self): """Get whether the switch is in on state.""" - return False + if self.tahoma_device.type == 'rts:GarageDoor4TRTSComponent': + return False + return bool(self._state == STATE_ON) + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attr = {} + super_attr = super().device_state_attributes + if super_attr is not None: + attr.update(super_attr) + + if 'core:RSSILevelState' in self.tahoma_device.active_states: + attr[ATTR_RSSI_LEVEL] = \ + self.tahoma_device.active_states['core:RSSILevelState'] + return attr + + @property + def available(self): + """Return True if entity is available.""" + return self._available diff --git a/homeassistant/components/tahoma.py b/homeassistant/components/tahoma.py index aaa64489168..64071ddb037 100644 --- a/homeassistant/components/tahoma.py +++ b/homeassistant/components/tahoma.py @@ -52,6 +52,7 @@ TAHOMA_TYPES = { 'rts:GarageDoor4TRTSComponent': 'switch', 'io:VerticalExteriorAwningIOComponent': 'cover', 'io:HorizontalAwningIOComponent': 'cover', + 'io:OnOffLightIOComponent': 'switch', 'rtds:RTDSSmokeSensor': 'smoke', } From 03480dc779e2837cec0f4db16b8270a31257d86d Mon Sep 17 00:00:00 2001 From: Totoo Date: Sun, 2 Sep 2018 18:58:31 +0200 Subject: [PATCH 097/172] Update discord.py (#16248) * Update discord.py * Update discord.py Fixed ATTR_IMAGES checking, line length, and ATTR_DATA imported. Also fixed missing spaces. * Update discord.py Fix E302... --- homeassistant/components/notify/discord.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/notify/discord.py b/homeassistant/components/notify/discord.py index dca47a46dbf..0cf4bced360 100644 --- a/homeassistant/components/notify/discord.py +++ b/homeassistant/components/notify/discord.py @@ -11,7 +11,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( - PLATFORM_SCHEMA, BaseNotificationService, ATTR_TARGET) + PLATFORM_SCHEMA, BaseNotificationService, ATTR_TARGET, ATTR_DATA) from homeassistant.const import CONF_TOKEN _LOGGER = logging.getLogger(__name__) @@ -22,6 +22,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_TOKEN): cv.string }) +ATTR_IMAGES = 'images' + def get_service(hass, config, discovery_info=None): """Get the Discord notification service.""" @@ -53,9 +55,15 @@ class DiscordNotificationService(BaseNotificationService): def on_ready(): """Send the messages when the bot is ready.""" try: + data = kwargs.get(ATTR_DATA) + if data: + images = data.get(ATTR_IMAGES) for channelid in kwargs[ATTR_TARGET]: channel = discord.Object(id=channelid) yield from discord_bot.send_message(channel, message) + if images: + for anum, f_name in enumerate(images): + yield from discord_bot.send_file(channel, f_name) except (discord.errors.HTTPException, discord.errors.NotFound) as error: _LOGGER.warning("Communication error: %s", error) From ac3700d1c46959bb572b5d0b73936fa0491806c7 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 2 Sep 2018 19:01:25 +0200 Subject: [PATCH 098/172] Upgrade python-telegram-bot to 11.0.0 (#16373) --- homeassistant/components/telegram_bot/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 53695102601..8e24716ab57 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -22,7 +22,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import TemplateError from homeassistant.setup import async_prepare_setup_platform -REQUIREMENTS = ['python-telegram-bot==10.1.0'] +REQUIREMENTS = ['python-telegram-bot==11.0.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index b2ac021d61e..cf761ef4aa6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1139,7 +1139,7 @@ python-synology==0.2.0 python-tado==0.2.3 # homeassistant.components.telegram_bot -python-telegram-bot==10.1.0 +python-telegram-bot==11.0.0 # homeassistant.components.sensor.twitch python-twitch==1.3.0 From 78fcea25bbfcf36e8b646941896f9409169ac5b1 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 2 Sep 2018 19:01:43 +0200 Subject: [PATCH 099/172] Upgrade attrs to 18.2.0 (#16372) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3e9a763181a..8d49d5d07c6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,7 +1,7 @@ aiohttp==3.4.0 astral==1.6.1 async_timeout==3.0.0 -attrs==18.1.0 +attrs==18.2.0 bcrypt==3.1.4 certifi>=2018.04.16 jinja2>=2.10 diff --git a/requirements_all.txt b/requirements_all.txt index cf761ef4aa6..41814e84b64 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2,7 +2,7 @@ aiohttp==3.4.0 astral==1.6.1 async_timeout==3.0.0 -attrs==18.1.0 +attrs==18.2.0 bcrypt==3.1.4 certifi>=2018.04.16 jinja2>=2.10 diff --git a/setup.py b/setup.py index b1b0af70319..71f63adfb64 100755 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ REQUIRES = [ 'aiohttp==3.4.0', 'astral==1.6.1', 'async_timeout==3.0.0', - 'attrs==18.1.0', + 'attrs==18.2.0', 'bcrypt==3.1.4', 'certifi>=2018.04.16', 'jinja2>=2.10', From 4685a2cd97452ce5ecd679af1a3bba3602be20e9 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Sun, 2 Sep 2018 13:17:29 -0700 Subject: [PATCH 100/172] Update server.py (#16375) --- homeassistant/components/mqtt/server.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/mqtt/server.py b/homeassistant/components/mqtt/server.py index 57da85fe5f6..45529411ed5 100644 --- a/homeassistant/components/mqtt/server.py +++ b/homeassistant/components/mqtt/server.py @@ -85,6 +85,10 @@ def generate_config(hass, passwd, password): 'allow-anonymous': password is None }, 'plugins': ['auth_anonymous'], + 'topic-check': { + 'enabled': True, + 'plugins': ['topic_taboo'], + }, } if password: From 1966597d5ef4fd8536401d3531ebd219aa9f4de4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sun, 2 Sep 2018 23:05:48 +0200 Subject: [PATCH 101/172] add_entities for switchmate (#16368) --- homeassistant/components/switch/switchmate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/switchmate.py b/homeassistant/components/switch/switchmate.py index 6ce4421ebc8..7ccd3bee4b6 100644 --- a/homeassistant/components/switch/switchmate.py +++ b/homeassistant/components/switch/switchmate.py @@ -31,11 +31,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def setup_platform(hass, config, add_devices, discovery_info=None) -> None: +def setup_platform(hass, config, add_entities, discovery_info=None) -> None: """Perform the setup for Switchmate devices.""" name = config.get(CONF_NAME) mac_addr = config.get(CONF_MAC) - add_devices([Switchmate(mac_addr, name)], True) + add_entities([Switchmate(mac_addr, name)], True) class Switchmate(SwitchDevice): From bf29cbd38184310960f26606d3d4ebcb69066178 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 3 Sep 2018 13:20:57 +0200 Subject: [PATCH 102/172] Update frontend to 20180903.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 5508aa76acf..72f61ddf1eb 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -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==20180831.0'] +REQUIREMENTS = ['home-assistant-frontend==20180903.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 41814e84b64..6223f9cc51c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -448,7 +448,7 @@ hole==0.3.0 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180831.0 +home-assistant-frontend==20180903.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 446e3d056a3..0ee02e0d109 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -84,7 +84,7 @@ hbmqtt==0.9.4 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180831.0 +home-assistant-frontend==20180903.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From a2a447b466f658d47aea9719297466d02100c775 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 3 Sep 2018 13:21:37 +0200 Subject: [PATCH 103/172] Update translations --- .../components/auth/.translations/hu.json | 16 ++++++++++ .../components/auth/.translations/ko.json | 2 +- .../components/cast/.translations/fr.json | 6 ++-- .../components/deconz/.translations/no.json | 2 +- .../components/hangouts/.translations/fr.json | 7 +++-- .../components/hangouts/.translations/hu.json | 29 +++++++++++++++++++ .../hangouts/.translations/pt-BR.json | 6 +++- .../homematicip_cloud/.translations/no.json | 2 +- .../components/nest/.translations/fr.json | 18 +++++++++++- .../sensor/.translations/moon.hu.json | 12 ++++++++ .../sensor/.translations/moon.no.json | 4 +-- .../sensor/.translations/moon.pt-BR.json | 6 ++-- .../components/sonos/.translations/fr.json | 13 +++++++++ 13 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 homeassistant/components/auth/.translations/hu.json create mode 100644 homeassistant/components/hangouts/.translations/hu.json create mode 100644 homeassistant/components/sensor/.translations/moon.hu.json create mode 100644 homeassistant/components/sonos/.translations/fr.json diff --git a/homeassistant/components/auth/.translations/hu.json b/homeassistant/components/auth/.translations/hu.json new file mode 100644 index 00000000000..4500098553e --- /dev/null +++ b/homeassistant/components/auth/.translations/hu.json @@ -0,0 +1,16 @@ +{ + "mfa_setup": { + "totp": { + "error": { + "invalid_code": "\u00c9rv\u00e9nytelen k\u00f3d, pr\u00f3b\u00e1ld \u00fajra. Ha ez a hiba folyamatosan el\u0151fordul, akkor gy\u0151z\u0151dj meg r\u00f3la, hogy a Home Assistant rendszered \u00f3r\u00e1ja pontosan j\u00e1r." + }, + "step": { + "init": { + "description": "Ahhoz, hogy haszn\u00e1lhasd a k\u00e9tfaktoros hiteles\u00edt\u00e9st id\u0151alap\u00fa egyszeri jelszavakkal, szkenneld be a QR k\u00f3dot a hiteles\u00edt\u00e9si applik\u00e1ci\u00f3ddal. Ha m\u00e9g nincsen, akkor a [Google Hiteles\u00edt\u0151](https://support.google.com/accounts/answer/1066447)t vagy az [Authy](https://authy.com/)-t aj\u00e1nljuk.\n\n{qr_code}\n\nA k\u00f3d beolvas\u00e1sa ut\u00e1n add meg a hat sz\u00e1mjegy\u0171 k\u00f3dot az applik\u00e1ci\u00f3b\u00f3l a telep\u00edt\u00e9s ellen\u0151rz\u00e9s\u00e9hez. Ha probl\u00e9m\u00e1ba \u00fctk\u00f6z\u00f6l a QR k\u00f3d beolvas\u00e1s\u00e1n\u00e1l, akkor ind\u00edts egy k\u00e9zi be\u00e1ll\u00edt\u00e1st a **`{code}`** k\u00f3ddal.", + "title": "K\u00e9tfaktoros hiteles\u00edt\u00e9s be\u00e1ll\u00edt\u00e1sa TOTP haszn\u00e1lat\u00e1val" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/ko.json b/homeassistant/components/auth/.translations/ko.json index 4eb4783edd9..17fb5c56f57 100644 --- a/homeassistant/components/auth/.translations/ko.json +++ b/homeassistant/components/auth/.translations/ko.json @@ -6,7 +6,7 @@ }, "step": { "init": { - "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574 \uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.", + "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574 \uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.", "title": "TOTP \ub97c \uc0ac\uc6a9\ud558\uc5ec 2 \ub2e8\uacc4 \uc778\uc99d \uad6c\uc131" } }, diff --git a/homeassistant/components/cast/.translations/fr.json b/homeassistant/components/cast/.translations/fr.json index acacddf2187..d3b95121de6 100644 --- a/homeassistant/components/cast/.translations/fr.json +++ b/homeassistant/components/cast/.translations/fr.json @@ -6,8 +6,10 @@ }, "step": { "confirm": { - "description": "Voulez-vous configurer Google Cast?" + "description": "Voulez-vous configurer Google Cast?", + "title": "Google Cast" } - } + }, + "title": "Google Cast" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index 55518b7da53..27868814eab 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -28,6 +28,6 @@ "title": "Ekstra konfigurasjonsalternativer for deCONZ" } }, - "title": "deCONZ" + "title": "deCONZ Zigbee gateway" } } \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/fr.json b/homeassistant/components/hangouts/.translations/fr.json index c92d478c454..53759f9b534 100644 --- a/homeassistant/components/hangouts/.translations/fr.json +++ b/homeassistant/components/hangouts/.translations/fr.json @@ -13,9 +13,12 @@ }, "user": { "data": { + "email": "Adresse e-mail", "password": "Mot de passe" - } + }, + "title": "Connexion \u00e0 Google Hangouts" } - } + }, + "title": "Google Hangouts" } } \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/hu.json b/homeassistant/components/hangouts/.translations/hu.json new file mode 100644 index 00000000000..2631843c784 --- /dev/null +++ b/homeassistant/components/hangouts/.translations/hu.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "A Google Hangouts m\u00e1r konfigur\u00e1lva van", + "unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt." + }, + "error": { + "invalid_2fa": "\u00c9rv\u00e9nytelen K\u00e9tfaktoros hiteles\u00edt\u00e9s, pr\u00f3b\u00e1ld \u00fajra.", + "invalid_2fa_method": "\u00c9rv\u00e9nytelen 2FA M\u00f3dszer (Ellen\u0151rz\u00e9s a Telefonon).", + "invalid_login": "\u00c9rv\u00e9nytelen bejelentkez\u00e9s, pr\u00f3b\u00e1ld \u00fajra." + }, + "step": { + "2fa": { + "data": { + "2fa": "2FA Pin" + }, + "title": "K\u00e9tfaktoros Hiteles\u00edt\u00e9s" + }, + "user": { + "data": { + "email": "E-Mail C\u00edm", + "password": "Jelsz\u00f3" + }, + "title": "Google Hangouts Bejelentkez\u00e9s" + } + }, + "title": "Google Hangouts" + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/pt-BR.json b/homeassistant/components/hangouts/.translations/pt-BR.json index 41b097f3f8d..516229c3871 100644 --- a/homeassistant/components/hangouts/.translations/pt-BR.json +++ b/homeassistant/components/hangouts/.translations/pt-BR.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_configured": "Hangouts do Google j\u00e1 est\u00e1 configurado." + "already_configured": "Hangouts do Google j\u00e1 est\u00e1 configurado.", + "unknown": "Ocorreu um erro desconhecido." + }, + "error": { + "invalid_2fa": "Autentica\u00e7\u00e3o de 2 fatores inv\u00e1lida, por favor, tente novamente." }, "step": { "2fa": { diff --git a/homeassistant/components/homematicip_cloud/.translations/no.json b/homeassistant/components/homematicip_cloud/.translations/no.json index a310a918f64..730f00ae625 100644 --- a/homeassistant/components/homematicip_cloud/.translations/no.json +++ b/homeassistant/components/homematicip_cloud/.translations/no.json @@ -22,7 +22,7 @@ "title": "Velg HomematicIP tilgangspunkt" }, "link": { - "description": "Trykk p\u00e5 den bl\u00e5 knappen p\u00e5 tilgangspunktet og send knappen for \u00e5 registrere HomematicIP med Home Assistant. \n\n![Plassering av knapp p\u00e5 bridge](/static/images/config_flows/config_homematicip_cloud.png)", + "description": "Trykk p\u00e5 den bl\u00e5 knappen p\u00e5 tilgangspunktet og p\u00e5 send knappen for \u00e5 registrere HomematicIP med Home Assistant. \n\n![Plassering av knapp p\u00e5 bridge](/static/images/config_flows/config_homematicip_cloud.png)", "title": "Link tilgangspunkt" } }, diff --git a/homeassistant/components/nest/.translations/fr.json b/homeassistant/components/nest/.translations/fr.json index 62a4d7deec9..734e82dbcd0 100644 --- a/homeassistant/components/nest/.translations/fr.json +++ b/homeassistant/components/nest/.translations/fr.json @@ -2,6 +2,22 @@ "config": { "abort": { "already_setup": "Vous ne pouvez configurer qu'un seul compte Nest." - } + }, + "error": { + "internal_error": "Erreur interne lors de la validation du code", + "invalid_code": "Code invalide" + }, + "step": { + "init": { + "title": "Fournisseur d'authentification" + }, + "link": { + "data": { + "code": "Code PIN" + }, + "title": "Lier un compte Nest" + } + }, + "title": "Nest" } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.hu.json b/homeassistant/components/sensor/.translations/moon.hu.json new file mode 100644 index 00000000000..0fcd02a6961 --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.hu.json @@ -0,0 +1,12 @@ +{ + "state": { + "first_quarter": "Els\u0151 negyed", + "full_moon": "Telihold", + "last_quarter": "Utols\u00f3 negyed", + "new_moon": "\u00dajhold", + "waning_crescent": "Fogy\u00f3 Hold (sarl\u00f3)", + "waning_gibbous": "Fogy\u00f3 Hold", + "waxing_crescent": "N\u00f6v\u0151 Hold (sarl\u00f3)", + "waxing_gibbous": "N\u00f6v\u0151 Hold" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.no.json b/homeassistant/components/sensor/.translations/moon.no.json index 104412c90ba..19f9985accb 100644 --- a/homeassistant/components/sensor/.translations/moon.no.json +++ b/homeassistant/components/sensor/.translations/moon.no.json @@ -1,8 +1,8 @@ { "state": { - "first_quarter": "F\u00f8rste kvartdel", + "first_quarter": "F\u00f8rste kvarter", "full_moon": "Fullm\u00e5ne", - "last_quarter": "Siste kvartdel", + "last_quarter": "Siste kvarter", "new_moon": "Nym\u00e5ne", "waning_crescent": "Minkende halvm\u00e5ne", "waning_gibbous": "Minkende trekvartm\u00e5ne", diff --git a/homeassistant/components/sensor/.translations/moon.pt-BR.json b/homeassistant/components/sensor/.translations/moon.pt-BR.json index 57d3a3e95e4..93b17784a4e 100644 --- a/homeassistant/components/sensor/.translations/moon.pt-BR.json +++ b/homeassistant/components/sensor/.translations/moon.pt-BR.json @@ -1,7 +1,9 @@ { "state": { - "full_moon": "Cheia", - "new_moon": "Nova", + "first_quarter": "Quarto crescente", + "full_moon": "Lua cheia", + "last_quarter": "Quarto minguante", + "new_moon": "Lua Nova", "waning_crescent": "Minguante", "waning_gibbous": "Minguante gibosa", "waxing_crescent": "Crescente", diff --git a/homeassistant/components/sonos/.translations/fr.json b/homeassistant/components/sonos/.translations/fr.json new file mode 100644 index 00000000000..768a798e6d5 --- /dev/null +++ b/homeassistant/components/sonos/.translations/fr.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Aucun p\u00e9riph\u00e9rique Sonos trouv\u00e9 sur le r\u00e9seau.", + "single_instance_allowed": "Seulement une seule configuration de Sonos est n\u00e9cessaire." + }, + "step": { + "confirm": { + "description": "Voulez-vous configurer Sonos?" + } + } + } +} \ No newline at end of file From 00cba29ae189e3846a39beef7f830d909f4c6cb8 Mon Sep 17 00:00:00 2001 From: Pawel Date: Mon, 3 Sep 2018 21:40:04 +0200 Subject: [PATCH 104/172] Support for playing radio preset by Onkyo media_player (#16258) * Added support to play radio preset by play media * Switch radio station by preset for Onkyo * added SUPPORT_PLAY_MEDIA --- homeassistant/components/media_player/onkyo.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/media_player/onkyo.py b/homeassistant/components/media_player/onkyo.py index af9a6ef54ce..00df456804f 100644 --- a/homeassistant/components/media_player/onkyo.py +++ b/homeassistant/components/media_player/onkyo.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant.components.media_player import ( SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, SUPPORT_PLAY, - MediaPlayerDevice, PLATFORM_SCHEMA) + SUPPORT_PLAY_MEDIA, MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.const import (STATE_OFF, STATE_ON, CONF_HOST, CONF_NAME) import homeassistant.helpers.config_validation as cv @@ -30,17 +30,19 @@ SUPPORTED_MAX_VOLUME = 80 SUPPORT_ONKYO = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_VOLUME_STEP | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ - SUPPORT_SELECT_SOURCE | SUPPORT_PLAY + SUPPORT_SELECT_SOURCE | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA SUPPORT_ONKYO_WO_VOLUME = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ - SUPPORT_SELECT_SOURCE | SUPPORT_PLAY + SUPPORT_SELECT_SOURCE | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA KNOWN_HOSTS = [] # type: List[str] DEFAULT_SOURCES = {'tv': 'TV', 'bd': 'Bluray', 'game': 'Game', 'aux1': 'Aux1', 'video1': 'Video 1', 'video2': 'Video 2', 'video3': 'Video 3', 'video4': 'Video 4', 'video5': 'Video 5', 'video6': 'Video 6', - 'video7': 'Video 7'} + 'video7': 'Video 7', 'fm': 'Radio'} + +DEFAULT_PLAYABLE_SOURCES = ("fm", "am", "tuner") PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOST): cv.string, @@ -266,6 +268,13 @@ class OnkyoDevice(MediaPlayerDevice): source = self._reverse_mapping[source] self.command('input-selector {}'.format(source)) + def play_media(self, media_type, media_id, **kwargs): + """Play radio station by preset number.""" + source = self._reverse_mapping[self._current_source] + if (media_type.lower() == 'radio' and + source in DEFAULT_PLAYABLE_SOURCES): + self.command('preset {}'.format(media_id)) + class OnkyoDeviceZone(OnkyoDevice): """Representation of an Onkyo device's extra zone.""" From 2252f4a25797ba423bd452682c8b2d8048e26fb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Tue, 4 Sep 2018 01:11:40 +0200 Subject: [PATCH 105/172] Bug fix for Tibber (#16397) --- homeassistant/components/sensor/tibber.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/tibber.py b/homeassistant/components/sensor/tibber.py index 3670a5a59bd..ebc38fcb739 100644 --- a/homeassistant/components/sensor/tibber.py +++ b/homeassistant/components/sensor/tibber.py @@ -151,7 +151,7 @@ class TibberSensor(Entity): if now.date() == price_time.date(): max_price = max(max_price, price_total) min_price = min(min_price, price_total) - self._state = state - self._device_state_attributes['max_price'] = max_price - self._device_state_attributes['min_price'] = min_price + self._state = state + self._device_state_attributes['max_price'] = max_price + self._device_state_attributes['min_price'] = min_price return state is not None From ba63a6abc0e46c632041823b7b7900ab13b2611f Mon Sep 17 00:00:00 2001 From: Russell Cloran Date: Mon, 3 Sep 2018 22:46:27 -0700 Subject: [PATCH 106/172] zha: Bump to zigpy 0.2.0/bellows 0.7.0 (#16404) --- homeassistant/components/zha/__init__.py | 4 ++-- requirements_all.txt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index f17e7f02344..7aec4333ea8 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -16,8 +16,8 @@ from homeassistant.helpers import discovery, entity from homeassistant.util import slugify REQUIREMENTS = [ - 'bellows==0.6.0', - 'zigpy==0.1.0', + 'bellows==0.7.0', + 'zigpy==0.2.0', 'zigpy-xbee==0.1.1', ] diff --git a/requirements_all.txt b/requirements_all.txt index 6223f9cc51c..d8d73cc36a1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ batinfo==0.4.2 beautifulsoup4==4.6.3 # homeassistant.components.zha -bellows==0.6.0 +bellows==0.7.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.5.1 @@ -1530,4 +1530,4 @@ ziggo-mediabox-xl==1.0.0 zigpy-xbee==0.1.1 # homeassistant.components.zha -zigpy==0.1.0 +zigpy==0.2.0 From a4aa30fc73a625a5567b68bc6d06733694b8efcd Mon Sep 17 00:00:00 2001 From: Rene Nulsch <33263735+ReneNulschDE@users.noreply.github.com> Date: Tue, 4 Sep 2018 08:46:04 +0200 Subject: [PATCH 107/172] Fix SystemMonitor IP address sensor (#16394) --- homeassistant/components/sensor/systemmonitor.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index aa448ddf56e..de8e9783f92 100644 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/sensor.systemmonitor/ """ import logging import os +import socket import voluptuous as vol @@ -61,9 +62,9 @@ IO_COUNTER = { 'packets_in': 3, } -IF_ADDRS = { - 'ipv4_address': 0, - 'ipv6_address': 1, +IF_ADDRS_FAMILY = { + 'ipv4_address': socket.AF_INET, + 'ipv6_address': socket.AF_INET6, } @@ -165,7 +166,9 @@ class SystemMonitorSensor(Entity): elif self.type == 'ipv4_address' or self.type == 'ipv6_address': addresses = psutil.net_if_addrs() if self.argument in addresses: - self._state = addresses[self.argument][IF_ADDRS[self.type]][1] + for addr in addresses[self.argument]: + if addr.family == IF_ADDRS_FAMILY[self.type]: + self._state = addr.address else: self._state = None elif self.type == 'last_boot': From 7ea482cb1d6b7cb247dc105fd780416e8fd0ab51 Mon Sep 17 00:00:00 2001 From: 9R Date: Tue, 4 Sep 2018 08:48:03 +0200 Subject: [PATCH 108/172] add ExpressBus icon key to sensor.mvg (#16387) --- homeassistant/components/sensor/mvglive.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/mvglive.py b/homeassistant/components/sensor/mvglive.py index a7a4b592664..8634e4f4570 100644 --- a/homeassistant/components/sensor/mvglive.py +++ b/homeassistant/components/sensor/mvglive.py @@ -31,12 +31,13 @@ CONF_PRODUCTS = 'products' CONF_TIMEOFFSET = 'timeoffset' CONF_NUMBER = 'number' -DEFAULT_PRODUCT = ['U-Bahn', 'Tram', 'Bus', 'S-Bahn'] +DEFAULT_PRODUCT = ['U-Bahn', 'Tram', 'Bus', 'ExpressBus', 'S-Bahn'] ICONS = { 'U-Bahn': 'mdi:subway', 'Tram': 'mdi:tram', 'Bus': 'mdi:bus', + 'ExpressBus': 'mdi:bus', 'S-Bahn': 'mdi:train', 'SEV': 'mdi:checkbox-blank-circle-outline', '-': 'mdi:clock' From 7a6facc875ac0f3af33450d77880777ffcb3816d Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 4 Sep 2018 09:00:14 +0200 Subject: [PATCH 109/172] Device and entity registry remove config entry on unload (#16247) * Test * Ability to remove device * Don't remove devices, instead remove config entry from device and entity registries * Remove print * Remove is not the same as unload * Add tests * Fix hound comment --- homeassistant/config_entries.py | 8 +++++++ homeassistant/helpers/device_registry.py | 10 ++++++++- homeassistant/helpers/entity_registry.py | 10 ++++++++- tests/helpers/test_device_registry.py | 27 ++++++++++++++++++++++++ tests/helpers/test_entity_registry.py | 9 ++++++++ 5 files changed, 62 insertions(+), 2 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 8db09cdb8da..6eae9e13030 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -325,6 +325,14 @@ class ConfigEntries: unloaded = await entry.async_unload(self.hass) + device_registry = await \ + self.hass.helpers.device_registry.async_get_registry() + device_registry.async_clear_config_entry(entry_id) + + entity_registry = await \ + self.hass.helpers.entity_registry.async_get_registry() + entity_registry.async_clear_config_entry(entry_id) + return { 'require_restart': not unloaded } diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 504448b948d..e6ff45af2fe 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -45,7 +45,7 @@ class DeviceRegistry: self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) @callback - def async_get_device(self, identifiers: str, connections: tuple): + def async_get_device(self, identifiers: set, connections: set): """Check if device is registered.""" for device in self.devices.values(): if any(iden in device.identifiers for iden in identifiers) or \ @@ -127,6 +127,14 @@ class DeviceRegistry: return data + @callback + def async_clear_config_entry(self, config_entry): + """Clear config entry from registry entries.""" + for device in self.devices.values(): + if config_entry in device.config_entries: + device.config_entries.remove(config_entry) + self.async_schedule_save() + @bind_hass async def async_get_registry(hass) -> DeviceRegistry: diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 804ee4235d0..da3645a96fe 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -31,7 +31,7 @@ STORAGE_VERSION = 1 STORAGE_KEY = 'core.entity_registry' -@attr.s(slots=True, frozen=True) +@attr.s(slots=True) class RegistryEntry: """Entity Registry Entry.""" @@ -250,6 +250,14 @@ class EntityRegistry: return data + @callback + def async_clear_config_entry(self, config_entry): + """Clear config entry from registry entries.""" + for entry in self.entities.values(): + if config_entry == entry.config_entry_id: + entry.config_entry_id = None + self.async_schedule_save() + @bind_hass async def async_get_registry(hass) -> EntityRegistry: diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 84ad54f7b82..a9132529bc3 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -138,3 +138,30 @@ async def test_loading_from_storage(hass, hass_storage): manufacturer='manufacturer', model='model') assert entry.id == 'abcdefghijklm' assert isinstance(entry.config_entries, set) + + +async def test_removing_config_entries(registry): + """Make sure we do not get duplicate entries.""" + entry = registry.async_get_or_create( + config_entry='123', + connections={('ethernet', '12:34:56:78:90:AB:CD:EF')}, + identifiers={('bridgeid', '0123')}, + manufacturer='manufacturer', model='model') + entry2 = registry.async_get_or_create( + config_entry='456', + connections={('ethernet', '12:34:56:78:90:AB:CD:EF')}, + identifiers={('bridgeid', '0123')}, + manufacturer='manufacturer', model='model') + entry3 = registry.async_get_or_create( + config_entry='123', + connections={('ethernet', '34:56:78:90:AB:CD:EF:12')}, + identifiers={('bridgeid', '4567')}, + manufacturer='manufacturer', model='model') + + assert len(registry.devices) == 2 + assert entry is entry2 + assert entry is not entry3 + assert entry.config_entries == {'123', '456'} + registry.async_clear_config_entry('123') + assert entry.config_entries == {'456'} + assert entry3.config_entries == set() diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index d0c088a6f69..bb28287ddd8 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -186,6 +186,15 @@ async def test_updating_config_entry_id(registry): assert entry2.config_entry_id == 'mock-id-2' +async def test_removing_config_entry_id(registry): + """Test that we update config entry id in registry.""" + entry = registry.async_get_or_create( + 'light', 'hue', '5678', config_entry_id='mock-id-1') + assert entry.config_entry_id == 'mock-id-1' + registry.async_clear_config_entry('mock-id-1') + assert entry.config_entry_id is None + + async def test_migration(hass): """Test migration from old data to new.""" old_conf = { From f96aee2832b8834e48c24ccd54c3d0218614a641 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 4 Sep 2018 01:22:44 -0600 Subject: [PATCH 110/172] Add config flow for OpenUV (#16159) * OpenUV config flow in place * Test folder in place * Owner-requested comments * Tests * More tests * Owner-requested changes (part 1 of 2) * Updated requirements * Owner-requested changes (2 of 2) * Removed unnecessary import * Bumping Travis * Updated requirements * More requirements * Updated tests * Owner-requested changes * Hound * Updated docstring --- .coveragerc | 8 +- .../components/binary_sensor/openuv.py | 27 ++-- .../components/openuv/.translations/en.json | 20 +++ .../{openuv.py => openuv/__init__.py} | 128 +++++++++++++----- .../components/openuv/config_flow.py | 73 ++++++++++ homeassistant/components/openuv/const.py | 3 + homeassistant/components/openuv/strings.json | 20 +++ homeassistant/components/sensor/openuv.py | 34 +++-- homeassistant/config_entries.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/openuv/__init__.py | 1 + tests/components/openuv/test_config_flow.py | 93 +++++++++++++ 14 files changed, 348 insertions(+), 66 deletions(-) create mode 100644 homeassistant/components/openuv/.translations/en.json rename homeassistant/components/{openuv.py => openuv/__init__.py} (57%) create mode 100644 homeassistant/components/openuv/config_flow.py create mode 100644 homeassistant/components/openuv/const.py create mode 100644 homeassistant/components/openuv/strings.json create mode 100644 tests/components/openuv/__init__.py create mode 100644 tests/components/openuv/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 39c31e4e40b..bd531e62f72 100644 --- a/.coveragerc +++ b/.coveragerc @@ -123,7 +123,7 @@ omit = homeassistant/components/hangouts/const.py homeassistant/components/hangouts/hangouts_bot.py homeassistant/components/hangouts/hangups_utils.py - homeassistant/components/*/hangouts.py + homeassistant/components/*/hangouts.py homeassistant/components/hdmi_cec.py homeassistant/components/*/hdmi_cec.py @@ -145,12 +145,12 @@ omit = homeassistant/components/ihc/* homeassistant/components/*/ihc.py - + homeassistant/components/insteon/* homeassistant/components/*/insteon.py homeassistant/components/insteon_local.py - + homeassistant/components/insteon_plm.py homeassistant/components/ios.py @@ -228,7 +228,7 @@ omit = homeassistant/components/opencv.py homeassistant/components/*/opencv.py - homeassistant/components/openuv.py + homeassistant/components/openuv/__init__.py homeassistant/components/*/openuv.py homeassistant/components/pilight.py diff --git a/homeassistant/components/binary_sensor/openuv.py b/homeassistant/components/binary_sensor/openuv.py index 0b299529a46..c7c27d73ee4 100644 --- a/homeassistant/components/binary_sensor/openuv.py +++ b/homeassistant/components/binary_sensor/openuv.py @@ -7,12 +7,11 @@ https://home-assistant.io/components/binary_sensor.openuv/ import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.components.openuv import ( - BINARY_SENSORS, DATA_PROTECTION_WINDOW, DOMAIN, TOPIC_UPDATE, - TYPE_PROTECTION_WINDOW, OpenUvEntity) + BINARY_SENSORS, DATA_OPENUV_CLIENT, DATA_PROTECTION_WINDOW, DOMAIN, + TOPIC_UPDATE, TYPE_PROTECTION_WINDOW, OpenUvEntity) from homeassistant.util.dt import as_local, parse_datetime, utcnow DEPENDENCIES = ['openuv'] @@ -26,17 +25,20 @@ ATTR_PROTECTION_WINDOW_ENDING_UV = 'end_uv' async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): - """Set up the OpenUV binary sensor platform.""" - if discovery_info is None: - return + """Set up an OpenUV sensor based on existing config.""" + pass - openuv = hass.data[DOMAIN] + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up an OpenUV sensor based on a config entry.""" + openuv = hass.data[DOMAIN][DATA_OPENUV_CLIENT][entry.entry_id] binary_sensors = [] - for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: + for sensor_type in openuv.binary_sensor_conditions: name, icon = BINARY_SENSORS[sensor_type] binary_sensors.append( - OpenUvBinarySensor(openuv, sensor_type, name, icon)) + OpenUvBinarySensor( + openuv, sensor_type, name, icon, entry.entry_id)) async_add_entities(binary_sensors, True) @@ -44,14 +46,16 @@ async def async_setup_platform( class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice): """Define a binary sensor for OpenUV.""" - def __init__(self, openuv, sensor_type, name, icon): + def __init__(self, openuv, sensor_type, name, icon, entry_id): """Initialize the sensor.""" super().__init__(openuv) + self._entry_id = entry_id self._icon = icon self._latitude = openuv.client.latitude self._longitude = openuv.client.longitude self._name = name + self._dispatch_remove = None self._sensor_type = sensor_type self._state = None @@ -83,8 +87,9 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice): async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect( + self._dispatch_remove = async_dispatcher_connect( self.hass, TOPIC_UPDATE, self._update_data) + self.async_on_remove(self._dispatch_remove) async def async_update(self): """Update the state.""" diff --git a/homeassistant/components/openuv/.translations/en.json b/homeassistant/components/openuv/.translations/en.json new file mode 100644 index 00000000000..df0232d01fc --- /dev/null +++ b/homeassistant/components/openuv/.translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "identifier_exists": "Coordinates already registered", + "invalid_api_key": "Invalid API key" + }, + "step": { + "user": { + "data": { + "api_key": "OpenUV API Key", + "elevation": "Elevation", + "latitude": "Latitude", + "longitude": "Longitude" + }, + "title": "Fill in your information" + } + }, + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv.py b/homeassistant/components/openuv/__init__.py similarity index 57% rename from homeassistant/components/openuv.py rename to homeassistant/components/openuv/__init__.py index d696f0e5100..bfd90b4a574 100644 --- a/homeassistant/components/openuv.py +++ b/homeassistant/components/openuv/__init__.py @@ -1,5 +1,5 @@ """ -Support for data from openuv.io. +Support for UV data from openuv.io. For more details about this component, please refer to the documentation at https://home-assistant.io/components/openuv/ @@ -9,21 +9,24 @@ from datetime import timedelta import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, CONF_BINARY_SENSORS, CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL, CONF_SENSORS) -from homeassistant.helpers import ( - aiohttp_client, config_validation as cv, discovery) +from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval -REQUIREMENTS = ['pyopenuv==1.0.1'] +from .config_flow import configured_instances +from .const import DOMAIN + +REQUIREMENTS = ['pyopenuv==1.0.4'] _LOGGER = logging.getLogger(__name__) -DOMAIN = 'openuv' - +DATA_OPENUV_CLIENT = 'data_client' +DATA_OPENUV_LISTENER = 'data_listener' DATA_PROTECTION_WINDOW = 'protection_window' DATA_UV = 'uv' @@ -82,39 +85,77 @@ SENSOR_SCHEMA = vol.Schema({ }) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_ELEVATION): float, - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_BINARY_SENSORS, default={}): BINARY_SENSOR_SCHEMA, - vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, - vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): - cv.time_period, - }) + DOMAIN: + vol.Schema({ + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_ELEVATION): float, + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_BINARY_SENSORS, default={}): + BINARY_SENSOR_SCHEMA, + vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): + cv.time_period, + }) }, extra=vol.ALLOW_EXTRA) async def async_setup(hass, config): """Set up the OpenUV component.""" - from pyopenuv import Client - from pyopenuv.errors import OpenUvError + hass.data[DOMAIN] = {} + hass.data[DOMAIN][DATA_OPENUV_CLIENT] = {} + hass.data[DOMAIN][DATA_OPENUV_LISTENER] = {} + + if DOMAIN not in config: + return True conf = config[DOMAIN] - api_key = conf[CONF_API_KEY] - elevation = conf.get(CONF_ELEVATION, hass.config.elevation) latitude = conf.get(CONF_LATITUDE, hass.config.latitude) longitude = conf.get(CONF_LONGITUDE, hass.config.longitude) + elevation = conf.get(CONF_ELEVATION, hass.config.elevation) + + identifier = '{0}, {1}'.format(latitude, longitude) + + if identifier not in configured_instances(hass): + hass.async_add_job( + hass.config_entries.flow.async_init( + DOMAIN, + context={'source': SOURCE_IMPORT}, + data={ + CONF_API_KEY: conf[CONF_API_KEY], + CONF_LATITUDE: latitude, + CONF_LONGITUDE: longitude, + CONF_ELEVATION: elevation, + CONF_BINARY_SENSORS: conf[CONF_BINARY_SENSORS], + CONF_SENSORS: conf[CONF_SENSORS], + })) + + hass.data[DOMAIN][CONF_SCAN_INTERVAL] = conf[CONF_SCAN_INTERVAL] + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up OpenUV as config entry.""" + from pyopenuv import Client + from pyopenuv.errors import OpenUvError try: websession = aiohttp_client.async_get_clientsession(hass) openuv = OpenUV( Client( - api_key, latitude, longitude, websession, altitude=elevation), - conf[CONF_BINARY_SENSORS][CONF_MONITORED_CONDITIONS] + - conf[CONF_SENSORS][CONF_MONITORED_CONDITIONS]) + config_entry.data[CONF_API_KEY], + config_entry.data.get(CONF_LATITUDE, hass.config.latitude), + config_entry.data.get(CONF_LONGITUDE, hass.config.longitude), + websession, + altitude=config_entry.data.get( + CONF_ELEVATION, hass.config.elevation)), + config_entry.data.get(CONF_BINARY_SENSORS, {}).get( + CONF_MONITORED_CONDITIONS, list(BINARY_SENSORS)), + config_entry.data.get(CONF_SENSORS, {}).get( + CONF_MONITORED_CONDITIONS, list(SENSORS))) await openuv.async_update() - hass.data[DOMAIN] = openuv + hass.data[DOMAIN][DATA_OPENUV_CLIENT][config_entry.entry_id] = openuv except OpenUvError as err: _LOGGER.error('An error occurred: %s', str(err)) hass.components.persistent_notification.create( @@ -125,13 +166,9 @@ async def async_setup(hass, config): notification_id=NOTIFICATION_ID) return False - for component, schema in [ - ('binary_sensor', conf[CONF_BINARY_SENSORS]), - ('sensor', conf[CONF_SENSORS]), - ]: - hass.async_create_task( - discovery.async_load_platform( - hass, component, DOMAIN, schema, config)) + for component in ('binary_sensor', 'sensor'): + hass.async_create_task(hass.config_entries.async_forward_entry_setup( + config_entry, component)) async def refresh_sensors(event_time): """Refresh OpenUV data.""" @@ -139,7 +176,25 @@ async def async_setup(hass, config): await openuv.async_update() async_dispatcher_send(hass, TOPIC_UPDATE) - async_track_time_interval(hass, refresh_sensors, conf[CONF_SCAN_INTERVAL]) + hass.data[DOMAIN][DATA_OPENUV_LISTENER][ + config_entry.entry_id] = async_track_time_interval( + hass, refresh_sensors, + hass.data[DOMAIN][CONF_SCAN_INTERVAL]) + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload an OpenUV config entry.""" + for component in ('binary_sensor', 'sensor'): + await hass.config_entries.async_forward_entry_unload( + config_entry, component) + + hass.data[DOMAIN][DATA_OPENUV_CLIENT].pop(config_entry.entry_id) + + remove_listener = hass.data[DOMAIN][DATA_OPENUV_LISTENER].pop( + config_entry.entry_id) + remove_listener() return True @@ -147,19 +202,20 @@ async def async_setup(hass, config): class OpenUV: """Define a generic OpenUV object.""" - def __init__(self, client, monitored_conditions): + def __init__(self, client, binary_sensor_conditions, sensor_conditions): """Initialize.""" - self._monitored_conditions = monitored_conditions + self.binary_sensor_conditions = binary_sensor_conditions self.client = client self.data = {} + self.sensor_conditions = sensor_conditions async def async_update(self): """Update sensor/binary sensor data.""" - if TYPE_PROTECTION_WINDOW in self._monitored_conditions: + if TYPE_PROTECTION_WINDOW in self.binary_sensor_conditions: data = await self.client.uv_protection_window() self.data[DATA_PROTECTION_WINDOW] = data - if any(c in self._monitored_conditions for c in SENSORS): + if any(c in self.sensor_conditions for c in SENSORS): data = await self.client.uv_index() self.data[DATA_UV] = data diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py new file mode 100644 index 00000000000..55ee566268e --- /dev/null +++ b/homeassistant/components/openuv/config_flow.py @@ -0,0 +1,73 @@ +"""Config flow to configure the OpenUV component.""" + +from collections import OrderedDict + +import voluptuous as vol + +from homeassistant import config_entries, data_entry_flow +from homeassistant.core import callback +from homeassistant.const import ( + CONF_API_KEY, CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE) +from homeassistant.helpers import aiohttp_client, config_validation as cv + +from .const import DOMAIN + + +@callback +def configured_instances(hass): + """Return a set of configured OpenUV instances.""" + return set( + '{0}, {1}'.format( + entry.data[CONF_LATITUDE], entry.data[CONF_LONGITUDE]) + for entry in hass.config_entries.async_entries(DOMAIN)) + + +@config_entries.HANDLERS.register(DOMAIN) +class OpenUvFlowHandler(data_entry_flow.FlowHandler): + """Handle an OpenUV config flow.""" + + VERSION = 1 + + def __init__(self): + """Initialize the config flow.""" + pass + + async def async_step_import(self, import_config): + """Import a config entry from configuration.yaml.""" + return await self.async_step_user(import_config) + + async def async_step_user(self, user_input=None): + """Handle the start of the config flow.""" + from pyopenuv.util import validate_api_key + + errors = {} + + if user_input is not None: + identifier = '{0}, {1}'.format( + user_input.get(CONF_LATITUDE, self.hass.config.latitude), + user_input.get(CONF_LONGITUDE, self.hass.config.longitude)) + + if identifier in configured_instances(self.hass): + errors['base'] = 'identifier_exists' + else: + websession = aiohttp_client.async_get_clientsession(self.hass) + api_key_validation = await validate_api_key( + user_input[CONF_API_KEY], websession) + if api_key_validation: + return self.async_create_entry( + title=identifier, + data=user_input, + ) + errors['base'] = 'invalid_api_key' + + data_schema = OrderedDict() + data_schema[vol.Required(CONF_API_KEY)] = str + data_schema[vol.Optional(CONF_LATITUDE)] = cv.latitude + data_schema[vol.Optional(CONF_LONGITUDE)] = cv.longitude + data_schema[vol.Optional(CONF_ELEVATION)] = vol.Coerce(float) + + return self.async_show_form( + step_id='user', + data_schema=vol.Schema(data_schema), + errors=errors, + ) diff --git a/homeassistant/components/openuv/const.py b/homeassistant/components/openuv/const.py new file mode 100644 index 00000000000..1aa3d2abcaa --- /dev/null +++ b/homeassistant/components/openuv/const.py @@ -0,0 +1,3 @@ +"""Define constants for the OpenUV component.""" + +DOMAIN = 'openuv' diff --git a/homeassistant/components/openuv/strings.json b/homeassistant/components/openuv/strings.json new file mode 100644 index 00000000000..9c5af45619e --- /dev/null +++ b/homeassistant/components/openuv/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "title": "OpenUV", + "step": { + "user": { + "title": "Fill in your information", + "data": { + "api_key": "OpenUV API Key", + "elevation": "Elevation", + "latitude": "Latitude", + "longitude": "Longitude" + } + } + }, + "error": { + "identifier_exists": "Coordinates already registered", + "invalid_api_key": "Invalid API key" + } + } +} diff --git a/homeassistant/components/sensor/openuv.py b/homeassistant/components/sensor/openuv.py index aaa04590b3f..22712aa306b 100644 --- a/homeassistant/components/sensor/openuv.py +++ b/homeassistant/components/sensor/openuv.py @@ -6,13 +6,12 @@ https://home-assistant.io/components/sensor.openuv/ """ import logging -from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.components.openuv import ( - DATA_UV, DOMAIN, SENSORS, TOPIC_UPDATE, TYPE_CURRENT_OZONE_LEVEL, - TYPE_CURRENT_UV_INDEX, TYPE_CURRENT_UV_LEVEL, TYPE_MAX_UV_INDEX, - TYPE_SAFE_EXPOSURE_TIME_1, TYPE_SAFE_EXPOSURE_TIME_2, + DATA_OPENUV_CLIENT, DATA_UV, DOMAIN, SENSORS, TOPIC_UPDATE, + TYPE_CURRENT_OZONE_LEVEL, TYPE_CURRENT_UV_INDEX, TYPE_CURRENT_UV_LEVEL, + TYPE_MAX_UV_INDEX, TYPE_SAFE_EXPOSURE_TIME_1, TYPE_SAFE_EXPOSURE_TIME_2, TYPE_SAFE_EXPOSURE_TIME_3, TYPE_SAFE_EXPOSURE_TIME_4, TYPE_SAFE_EXPOSURE_TIME_5, TYPE_SAFE_EXPOSURE_TIME_6, OpenUvEntity) from homeassistant.util.dt import as_local, parse_datetime @@ -40,16 +39,20 @@ UV_LEVEL_LOW = "Low" async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): - """Set up the OpenUV binary sensor platform.""" - if discovery_info is None: - return + """Set up an OpenUV sensor based on existing config.""" + pass - openuv = hass.data[DOMAIN] + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up a Nest sensor based on a config entry.""" + openuv = hass.data[DOMAIN][DATA_OPENUV_CLIENT][entry.entry_id] sensors = [] - for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: + for sensor_type in openuv.sensor_conditions: name, icon, unit = SENSORS[sensor_type] - sensors.append(OpenUvSensor(openuv, sensor_type, name, icon, unit)) + sensors.append( + OpenUvSensor( + openuv, sensor_type, name, icon, unit, entry.entry_id)) async_add_entities(sensors, True) @@ -57,10 +60,12 @@ async def async_setup_platform( class OpenUvSensor(OpenUvEntity): """Define a binary sensor for OpenUV.""" - def __init__(self, openuv, sensor_type, name, icon, unit): + def __init__(self, openuv, sensor_type, name, icon, unit, entry_id): """Initialize the sensor.""" super().__init__(openuv) + self._dispatch_remove = None + self._entry_id = entry_id self._icon = icon self._latitude = openuv.client.latitude self._longitude = openuv.client.longitude @@ -102,7 +107,9 @@ class OpenUvSensor(OpenUvEntity): async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect(self.hass, TOPIC_UPDATE, self._update_data) + self._dispatch_remove = async_dispatcher_connect( + self.hass, TOPIC_UPDATE, self._update_data) + self.async_on_remove(self._dispatch_remove) async def async_update(self): """Update the state.""" @@ -125,8 +132,7 @@ class OpenUvSensor(OpenUvEntity): elif self._sensor_type == TYPE_MAX_UV_INDEX: self._state = data['uv_max'] self._attrs.update({ - ATTR_MAX_UV_TIME: as_local( - parse_datetime(data['uv_max_time'])) + ATTR_MAX_UV_TIME: as_local(parse_datetime(data['uv_max_time'])) }) elif self._sensor_type in (TYPE_SAFE_EXPOSURE_TIME_1, TYPE_SAFE_EXPOSURE_TIME_2, diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 6eae9e13030..15932f2c3f8 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -141,6 +141,7 @@ FLOWS = [ 'homematicip_cloud', 'hue', 'nest', + 'openuv', 'sonos', 'zone', ] diff --git a/requirements_all.txt b/requirements_all.txt index d8d73cc36a1..b155612350b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -990,7 +990,7 @@ pynut2==2.1.2 pynx584==0.4 # homeassistant.components.openuv -pyopenuv==1.0.1 +pyopenuv==1.0.4 # homeassistant.components.iota pyota==2.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0ee02e0d109..b9e44445114 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -154,6 +154,9 @@ pymonoprice==0.3 # homeassistant.components.binary_sensor.nx584 pynx584==0.4 +# homeassistant.components.openuv +pyopenuv==1.0.4 + # homeassistant.auth.mfa_modules.totp # homeassistant.components.sensor.otp pyotp==2.2.6 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 4b694ec7ec0..fc8e67b1ab6 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -78,6 +78,7 @@ TEST_REQUIREMENTS = ( 'pylitejet', 'pymonoprice', 'pynx584', + 'pyopenuv', 'pyotp', 'pyqwikswitch', 'PyRMVtransport', diff --git a/tests/components/openuv/__init__.py b/tests/components/openuv/__init__.py new file mode 100644 index 00000000000..0e3595b1e51 --- /dev/null +++ b/tests/components/openuv/__init__.py @@ -0,0 +1 @@ +"""Define tests for the OpenUV component.""" diff --git a/tests/components/openuv/test_config_flow.py b/tests/components/openuv/test_config_flow.py new file mode 100644 index 00000000000..0e50bddabde --- /dev/null +++ b/tests/components/openuv/test_config_flow.py @@ -0,0 +1,93 @@ +"""Define tests for the OpenUV config flow.""" +from unittest.mock import patch + +from homeassistant import data_entry_flow +from homeassistant.components.openuv import DOMAIN, config_flow +from homeassistant.const import ( + CONF_API_KEY, CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE) + +from tests.common import MockConfigEntry, mock_coro + + +async def test_duplicate_error(hass): + """Test that errors are shown when duplicates are added.""" + conf = { + CONF_API_KEY: '12345abcde', + CONF_ELEVATION: 59.1234, + CONF_LATITUDE: 39.128712, + CONF_LONGITUDE: -104.9812612, + } + + MockConfigEntry(domain=DOMAIN, data=conf).add_to_hass(hass) + flow = config_flow.OpenUvFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=conf) + assert result['errors'] == {'base': 'identifier_exists'} + + +@patch('pyopenuv.util.validate_api_key', return_value=mock_coro(False)) +async def test_invalid_api_key(hass): + """Test that an invalid API key throws an error.""" + conf = { + CONF_API_KEY: '12345abcde', + CONF_ELEVATION: 59.1234, + CONF_LATITUDE: 39.128712, + CONF_LONGITUDE: -104.9812612, + } + + flow = config_flow.OpenUvFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=conf) + assert result['errors'] == {'base': 'invalid_api_key'} + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + flow = config_flow.OpenUvFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=None) + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'user' + + +@patch('pyopenuv.util.validate_api_key', return_value=mock_coro(True)) +async def test_step_import(hass): + """Test that the import step works.""" + conf = { + CONF_API_KEY: '12345abcde', + } + + flow = config_flow.OpenUvFlowHandler() + flow.hass = hass + + result = await flow.async_step_import(import_config=conf) + + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['title'] == '{0}, {1}'.format( + hass.config.latitude, hass.config.longitude) + assert result['data'] == conf + + +@patch('pyopenuv.util.validate_api_key', return_value=mock_coro(True)) +async def test_step_user(hass): + """Test that the user step works.""" + conf = { + CONF_API_KEY: '12345abcde', + CONF_ELEVATION: 59.1234, + CONF_LATITUDE: 39.128712, + CONF_LONGITUDE: -104.9812612, + } + + flow = config_flow.OpenUvFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=conf) + + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['title'] == '{0}, {1}'.format( + conf[CONF_LATITUDE], conf[CONF_LONGITUDE]) + assert result['data'] == conf From 8fa999258915f84c0941986f578d81bec7cf5114 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 4 Sep 2018 09:24:42 +0200 Subject: [PATCH 111/172] Service to load new deCONZ devices without restart (#16308) * New service to load new devices from deCONZ without restarting HASS * Do not use len to check if list is empty * Add support for scenes to be updated as well * Rework refresh devices method * Fix test --- homeassistant/components/deconz/__init__.py | 54 +++++++++++++++++-- homeassistant/components/deconz/services.yaml | 4 +- homeassistant/components/scene/deconz.py | 19 ++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/scene/test_deconz.py | 1 + 6 files changed, 70 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index e9f797d95f9..6ed0a6e2c11 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -24,7 +24,7 @@ from .const import ( CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER) -REQUIREMENTS = ['pydeconz==45'] +REQUIREMENTS = ['pydeconz==47'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -46,6 +46,8 @@ SERVICE_SCHEMA = vol.Schema({ vol.Required(SERVICE_DATA): dict, }) +SERVICE_DEVICE_REFRESH = 'device_refresh' + async def async_setup(hass, config): """Load configuration for deCONZ component. @@ -84,15 +86,17 @@ async def async_setup_entry(hass, config_entry): @callback def async_add_device_callback(device_type, device): """Handle event of new device creation in deCONZ.""" + if not isinstance(device, list): + device = [device] async_dispatcher_send( - hass, 'deconz_new_{}'.format(device_type), [device]) + hass, 'deconz_new_{}'.format(device_type), device) session = aiohttp_client.async_get_clientsession(hass) deconz = DeconzSession(hass.loop, session, **config_entry.data, async_add_device=async_add_device_callback) result = await deconz.async_load_parameters() + if result is False: - _LOGGER.error("Failed to communicate with deCONZ") return False hass.data[DOMAIN] = deconz @@ -149,16 +153,60 @@ async def async_setup_entry(hass, config_entry): data = call.data.get(SERVICE_DATA) deconz = hass.data[DOMAIN] if entity_id: + entities = hass.data.get(DATA_DECONZ_ID) + if entities: field = entities.get(entity_id) + if field is None: _LOGGER.error('Could not find the entity %s', entity_id) return + await deconz.async_put_state(field, data) + hass.services.async_register( DOMAIN, SERVICE_DECONZ, async_configure, schema=SERVICE_SCHEMA) + async def async_refresh_devices(call): + """Refresh available devices from deCONZ.""" + deconz = hass.data[DOMAIN] + + groups = list(deconz.groups.keys()) + lights = list(deconz.lights.keys()) + scenes = list(deconz.scenes.keys()) + sensors = list(deconz.sensors.keys()) + + if not await deconz.async_load_parameters(): + return + + async_add_device_callback( + 'group', [group + for group_id, group in deconz.groups.items() + if group_id not in groups] + ) + + async_add_device_callback( + 'light', [light + for light_id, light in deconz.lights.items() + if light_id not in lights] + ) + + async_add_device_callback( + 'scene', [scene + for scene_id, scene in deconz.scenes.items() + if scene_id not in scenes] + ) + + async_add_device_callback( + 'sensor', [sensor + for sensor_id, sensor in deconz.sensors.items() + if sensor_id not in sensors] + ) + + hass.services.async_register( + DOMAIN, SERVICE_DEVICE_REFRESH, async_refresh_devices) + @callback def deconz_shutdown(event): """ diff --git a/homeassistant/components/deconz/services.yaml b/homeassistant/components/deconz/services.yaml index 78bf7041a93..fa0fb8e14a4 100644 --- a/homeassistant/components/deconz/services.yaml +++ b/homeassistant/components/deconz/services.yaml @@ -1,4 +1,3 @@ - configure: description: Set attribute of device in deCONZ. See https://home-assistant.io/components/deconz/#device-services for details. fields: @@ -11,3 +10,6 @@ configure: data: description: Data is a json object with what data you want to alter. example: '{"on": true}' + +device_refresh: + description: Refresh device lists from deCONZ. \ No newline at end of file diff --git a/homeassistant/components/scene/deconz.py b/homeassistant/components/scene/deconz.py index 5af8f657206..b8fca6d8630 100644 --- a/homeassistant/components/scene/deconz.py +++ b/homeassistant/components/scene/deconz.py @@ -5,8 +5,10 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/scene.deconz/ """ from homeassistant.components.deconz import ( - DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) + DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) from homeassistant.components.scene import Scene +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect DEPENDENCIES = ['deconz'] @@ -19,12 +21,17 @@ async def async_setup_platform(hass, config, async_add_entities, async def async_setup_entry(hass, config_entry, async_add_entities): """Set up scenes for deCONZ component.""" - scenes = hass.data[DATA_DECONZ].scenes - entities = [] + @callback + def async_add_scene(scenes): + """Add scene from deCONZ.""" + entities = [] + for scene in scenes: + entities.append(DeconzScene(scene)) + async_add_entities(entities) + hass.data[DATA_DECONZ_UNSUB].append( + async_dispatcher_connect(hass, 'deconz_new_scene', async_add_scene)) - for scene in scenes.values(): - entities.append(DeconzScene(scene)) - async_add_entities(entities) + async_add_scene(hass.data[DATA_DECONZ].scenes.values()) class DeconzScene(Scene): diff --git a/requirements_all.txt b/requirements_all.txt index b155612350b..5f4fd64a6a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -820,7 +820,7 @@ pycsspeechtts==1.0.2 pydaikin==0.4 # homeassistant.components.deconz -pydeconz==45 +pydeconz==47 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b9e44445114..236033e2f19 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -139,7 +139,7 @@ py-canary==0.5.0 pyblackbird==0.5 # homeassistant.components.deconz -pydeconz==45 +pydeconz==47 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/tests/components/scene/test_deconz.py b/tests/components/scene/test_deconz.py index 53f25808be2..8c22f718fa0 100644 --- a/tests/components/scene/test_deconz.py +++ b/tests/components/scene/test_deconz.py @@ -33,6 +33,7 @@ async def setup_bridge(hass, data): return_value=mock_coro(data)): await bridge.async_load_parameters() hass.data[deconz.DOMAIN] = bridge + hass.data[deconz.DATA_DECONZ_UNSUB] = [] hass.data[deconz.DATA_DECONZ_ID] = {} config_entry = config_entries.ConfigEntry( 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') From e61ac1a4a1ec03f9852ff48e585a602d494bc80e Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Tue, 4 Sep 2018 01:31:45 -0700 Subject: [PATCH 112/172] Delegate mqtt topic match validation to the paho mqtt client (#16403) * Delegate mqtt match topics to the paho mqtt client * Fixing linting error with importing MQTTMatcher --- homeassistant/components/mqtt/__init__.py | 26 +++++++---------------- tests/components/mqtt/test_init.py | 9 ++++++++ 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 71be9c2435e..6bb08d7e8e5 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -13,7 +13,6 @@ import os import socket import time import ssl -import re import requests.certs import attr @@ -727,23 +726,14 @@ def _raise_on_error(result_code: int) -> None: def _match_topic(subscription: str, topic: str) -> bool: """Test if topic matches subscription.""" - reg_ex_parts = [] # type: List[str] - suffix = "" - if subscription.endswith('#'): - subscription = subscription[:-2] - suffix = "(.*)" - sub_parts = subscription.split('/') - for sub_part in sub_parts: - if sub_part == "+": - reg_ex_parts.append(r"([^\/]+)") - else: - reg_ex_parts.append(re.escape(sub_part)) - - reg_ex = "^" + (r'\/'.join(reg_ex_parts)) + suffix + "$" - - reg = re.compile(reg_ex) - - return reg.match(topic) is not None + from paho.mqtt.matcher import MQTTMatcher + matcher = MQTTMatcher() + matcher[subscription] = True + try: + next(matcher.iter_match(topic)) + return True + except StopIteration: + return False class MqttAvailability(Entity): diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index ecbc7cb9b02..51bd75f66e3 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -277,6 +277,15 @@ class TestMQTTCallbacks(unittest.TestCase): self.hass.block_till_done() self.assertEqual(0, len(self.calls)) + def test_subscribe_topic_level_wildcard_root_topic_no_subtree_match(self): + """Test the subscription of wildcard topics.""" + mqtt.subscribe(self.hass, 'test-topic/#', self.record_calls) + + fire_mqtt_message(self.hass, 'test-topic-123', 'test-payload') + + self.hass.block_till_done() + self.assertEqual(0, len(self.calls)) + def test_subscribe_topic_subtree_wildcard_subtree_topic(self): """Test the subscription of wildcard topics.""" mqtt.subscribe(self.hass, 'test-topic/#', self.record_calls) From 85658b6dd17a200b57b7ee5728a3009ce24b359d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Tue, 4 Sep 2018 10:50:12 +0200 Subject: [PATCH 113/172] Clean up dlink and some bug fix (#16346) * Update dlink.py * style * style --- homeassistant/components/switch/dlink.py | 80 +++++++++++++++--------- 1 file changed, 52 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/switch/dlink.py b/homeassistant/components/switch/dlink.py index f4eaefcae20..91ef546ea22 100644 --- a/homeassistant/components/switch/dlink.py +++ b/homeassistant/components/switch/dlink.py @@ -5,14 +5,17 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.dlink/ """ import logging +import urllib +from datetime import timedelta import voluptuous as vol -from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) -from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME) import homeassistant.helpers.config_validation as cv -from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN +from homeassistant.util import dt as dt_util +from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) +from homeassistant.const import (ATTR_TEMPERATURE, + CONF_HOST, CONF_NAME, CONF_PASSWORD, + CONF_USERNAME, TEMP_CELSIUS) REQUIREMENTS = ['pyW215==0.6.0'] @@ -23,9 +26,7 @@ DEFAULT_PASSWORD = '' DEFAULT_USERNAME = 'admin' CONF_USE_LEGACY_PROTOCOL = 'use_legacy_protocol' -ATTR_CURRENT_CONSUMPTION = 'power_consumption' ATTR_TOTAL_CONSUMPTION = 'total_consumption' -ATTR_TEMPERATURE = 'temperature' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -35,6 +36,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) +SCAN_INTERVAL = timedelta(minutes=2) + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up a D-Link Smart Plug.""" @@ -46,10 +49,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): use_legacy_protocol = config.get(CONF_USE_LEGACY_PROTOCOL) name = config.get(CONF_NAME) - data = SmartPlugData(SmartPlug(host, - password, - username, - use_legacy_protocol)) + smartplug = SmartPlug(host, + password, + username, + use_legacy_protocol) + data = SmartPlugData(smartplug) add_entities([SmartPlugSwitch(hass, data, name)], True) @@ -74,37 +78,28 @@ class SmartPlugSwitch(SwitchDevice): try: ui_temp = self.units.temperature(int(self.data.temperature), TEMP_CELSIUS) - temperature = "%i %s" % \ - (ui_temp, self.units.temperature_unit) + temperature = ui_temp except (ValueError, TypeError): - temperature = STATE_UNKNOWN + temperature = None try: - current_consumption = "%.2f W" % \ - float(self.data.current_consumption) - except ValueError: - current_consumption = STATE_UNKNOWN - - try: - total_consumption = "%.1f kWh" % \ - float(self.data.total_consumption) - except ValueError: - total_consumption = STATE_UNKNOWN + total_consumption = float(self.data.total_consumption) + except (ValueError, TypeError): + total_consumption = None attrs = { - ATTR_CURRENT_CONSUMPTION: current_consumption, ATTR_TOTAL_CONSUMPTION: total_consumption, - ATTR_TEMPERATURE: temperature + ATTR_TEMPERATURE: temperature, } return attrs @property - def current_power_watt(self): + def current_power_w(self): """Return the current power usage in Watt.""" try: return float(self.data.current_consumption) - except ValueError: + except (ValueError, TypeError): return None @property @@ -124,6 +119,11 @@ class SmartPlugSwitch(SwitchDevice): """Get the latest data from the smart plug and updates the states.""" self.data.update() + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self.data.available + class SmartPlugData: """Get the latest data from smart plug.""" @@ -135,10 +135,34 @@ class SmartPlugData: self.temperature = None self.current_consumption = None self.total_consumption = None + self.available = False + self._n_tried = 0 + self._last_tried = None def update(self): """Get the latest data from the smart plug.""" - self.state = self.smartplug.state + if self._last_tried is not None: + last_try_s = (dt_util.now() - self._last_tried).total_seconds()/60 + retry_seconds = min(self._n_tried*2, 10) - last_try_s + if self._n_tried > 0 and retry_seconds > 0: + _LOGGER.warning("Waiting %s s to retry", retry_seconds) + return + + _state = 'unknown' + try: + self._last_tried = dt_util.now() + _state = self.smartplug.state + except urllib.error.HTTPError: + _LOGGER.error("Dlink connection problem") + if _state == 'unknown': + self._n_tried += 1 + self.available = False + _LOGGER.warning("Failed to connect to dlink switch.") + return + self.state = _state + self.available = True + self.temperature = self.smartplug.temperature self.current_consumption = self.smartplug.current_consumption self.total_consumption = self.smartplug.total_consumption + self._n_tried = 0 From 3bd12fcef6afb25d7cbf76191f320571f7af01f7 Mon Sep 17 00:00:00 2001 From: Toon Willems Date: Tue, 4 Sep 2018 11:15:02 +0200 Subject: [PATCH 114/172] Implement correct state for RFlink cover (#16304) * implement correct state for rflink cover * Fix linting error * invert logic as local testing pointed out it should be reversed * add period at the end to satisfy the linter --- homeassistant/components/cover/rflink.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cover/rflink.py b/homeassistant/components/cover/rflink.py index e50fa488b92..41a4c2af045 100644 --- a/homeassistant/components/cover/rflink.py +++ b/homeassistant/components/cover/rflink.py @@ -92,9 +92,9 @@ class RflinkCover(RflinkCommand, CoverDevice): self.cancel_queued_send_commands() command = event['command'] - if command in ['on', 'allon']: + if command in ['on', 'allon', 'up']: self._state = True - elif command in ['off', 'alloff']: + elif command in ['off', 'alloff', 'down']: self._state = False @property @@ -105,7 +105,12 @@ class RflinkCover(RflinkCommand, CoverDevice): @property def is_closed(self): """Return if the cover is closed.""" - return None + return not self._state + + @property + def assumed_state(self): + """Return True because covers can be stopped midway.""" + return True def async_close_cover(self, **kwargs): """Turn the device close.""" From e1501c83f8d983becc557b7cb89ec746746495bb Mon Sep 17 00:00:00 2001 From: Paul Annekov Date: Tue, 4 Sep 2018 22:03:30 +0300 Subject: [PATCH 115/172] Fix Mi Flora median calculation (#16085) * fixed median was based on 1.5 minute interval, not 1 hour * ignore median and set state from first value, when previous state was None * update before add, removed unused 'retries' and 'ble_timeout', check if platform ready * added missing blank line * fixed too long line * using modern python 3.5 features, changed comment to be less verbose * continuation line fix * removed DEFAULT_SCAN_INTERVAL in favor of existing SCAN_INTERVAL --- homeassistant/components/sensor/miflora.py | 38 ++++++++++++---------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/sensor/miflora.py b/homeassistant/components/sensor/miflora.py index ced17512089..6f0fb3aba30 100644 --- a/homeassistant/components/sensor/miflora.py +++ b/homeassistant/components/sensor/miflora.py @@ -4,16 +4,19 @@ Support for Xiaomi Mi Flora BLE plant sensor. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.miflora/ """ +import asyncio +from datetime import timedelta import logging - import voluptuous as vol +import async_timeout from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.exceptions import PlatformNotReady from homeassistant.const import ( - CONF_FORCE_UPDATE, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_MAC -) + CONF_FORCE_UPDATE, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_MAC, + CONF_SCAN_INTERVAL) REQUIREMENTS = ['miflora==0.4.0'] @@ -21,19 +24,14 @@ REQUIREMENTS = ['miflora==0.4.0'] _LOGGER = logging.getLogger(__name__) CONF_ADAPTER = 'adapter' -CONF_CACHE = 'cache_value' CONF_MEDIAN = 'median' -CONF_RETRIES = 'retries' -CONF_TIMEOUT = 'timeout' DEFAULT_ADAPTER = 'hci0' -DEFAULT_UPDATE_INTERVAL = 1200 DEFAULT_FORCE_UPDATE = False DEFAULT_MEDIAN = 3 DEFAULT_NAME = 'Mi Flora' -DEFAULT_RETRIES = 2 -DEFAULT_TIMEOUT = 10 +SCAN_INTERVAL = timedelta(seconds=1200) # Sensor types are defined like: Name, units SENSOR_TYPES = { @@ -51,14 +49,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_MEDIAN, default=DEFAULT_MEDIAN): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - vol.Optional(CONF_RETRIES, default=DEFAULT_RETRIES): cv.positive_int, - vol.Optional(CONF_CACHE, default=DEFAULT_UPDATE_INTERVAL): cv.positive_int, vol.Optional(CONF_ADAPTER, default=DEFAULT_ADAPTER): cv.string, }) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the MiFlora sensor.""" from miflora import miflora_poller try: @@ -70,17 +66,22 @@ def setup_platform(hass, config, add_entities, discovery_info=None): backend = GatttoolBackend _LOGGER.debug('Miflora is using %s backend.', backend.__name__) - cache = config.get(CONF_CACHE) + cache = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL).total_seconds() poller = miflora_poller.MiFloraPoller( config.get(CONF_MAC), cache_timeout=cache, adapter=config.get(CONF_ADAPTER), backend=backend) force_update = config.get(CONF_FORCE_UPDATE) median = config.get(CONF_MEDIAN) - poller.ble_timeout = config.get(CONF_TIMEOUT) - poller.retries = config.get(CONF_RETRIES) devs = [] + try: + with async_timeout.timeout(9): + await hass.async_add_executor_job(poller.fill_cache) + except asyncio.TimeoutError: + _LOGGER.error('Unable to connect to %s', config.get(CONF_MAC)) + raise PlatformNotReady + for parameter in config[CONF_MONITORED_CONDITIONS]: name = SENSOR_TYPES[parameter][0] unit = SENSOR_TYPES[parameter][1] @@ -92,7 +93,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): devs.append(MiFloraSensor( poller, parameter, name, unit, force_update, median)) - add_entities(devs) + async_add_entities(devs, update_before_add=True) class MiFloraSensor(Entity): @@ -171,5 +172,8 @@ class MiFloraSensor(Entity): median = sorted(self.data)[int((self.median_count - 1) / 2)] _LOGGER.debug("Median is: %s", median) self._state = median + elif self._state is None: + _LOGGER.debug("Set initial state") + self._state = self.data[0] else: _LOGGER.debug("Not yet enough data for median calculation") From 746f4ac1585ac47be3bcc5d06d79b54da4a4e900 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Sep 2018 21:16:24 +0200 Subject: [PATCH 116/172] Add context to scripts and automations (#16415) * Add context to script helper * Update script component * Add context to automations * Lint --- .../components/automation/__init__.py | 91 ++++++++----------- homeassistant/components/automation/event.py | 4 +- .../components/automation/homeassistant.py | 8 +- .../components/automation/numeric_state.py | 4 +- homeassistant/components/automation/state.py | 4 +- .../components/automation/template.py | 4 +- homeassistant/components/automation/zone.py | 4 +- homeassistant/components/script.py | 57 +++++------- homeassistant/helpers/script.py | 46 ++++++---- homeassistant/helpers/service.py | 4 +- tests/components/automation/test_event.py | 7 +- .../automation/test_numeric_state.py | 10 +- tests/components/automation/test_state.py | 6 +- tests/components/automation/test_template.py | 12 +-- tests/components/automation/test_zone.py | 6 +- tests/components/test_script.py | 13 ++- tests/helpers/test_script.py | 28 ++++-- 17 files changed, 164 insertions(+), 144 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index c6c0af90d15..43fd4cedb88 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -158,27 +158,26 @@ def async_reload(hass): return hass.services.async_call(DOMAIN, SERVICE_RELOAD) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the automation.""" component = EntityComponent(_LOGGER, DOMAIN, hass, group_name=GROUP_NAME_ALL_AUTOMATIONS) - yield from _async_process_config(hass, config, component) + await _async_process_config(hass, config, component) - @asyncio.coroutine - def trigger_service_handler(service_call): + async def trigger_service_handler(service_call): """Handle automation triggers.""" tasks = [] for entity in component.async_extract_from_service(service_call): tasks.append(entity.async_trigger( - service_call.data.get(ATTR_VARIABLES), True)) + service_call.data.get(ATTR_VARIABLES), + skip_condition=True, + context=service_call.context)) if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) - @asyncio.coroutine - def turn_onoff_service_handler(service_call): + async def turn_onoff_service_handler(service_call): """Handle automation turn on/off service calls.""" tasks = [] method = 'async_{}'.format(service_call.service) @@ -186,10 +185,9 @@ def async_setup(hass, config): tasks.append(getattr(entity, method)()) if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) - @asyncio.coroutine - def toggle_service_handler(service_call): + async def toggle_service_handler(service_call): """Handle automation toggle service calls.""" tasks = [] for entity in component.async_extract_from_service(service_call): @@ -199,15 +197,14 @@ def async_setup(hass, config): tasks.append(entity.async_turn_on()) if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) - @asyncio.coroutine - def reload_service_handler(service_call): + async def reload_service_handler(service_call): """Remove all automations and load new ones from config.""" - conf = yield from component.async_prepare_reload() + conf = await component.async_prepare_reload() if conf is None: return - yield from _async_process_config(hass, conf, component) + await _async_process_config(hass, conf, component) hass.services.async_register( DOMAIN, SERVICE_TRIGGER, trigger_service_handler, @@ -272,15 +269,14 @@ class AutomationEntity(ToggleEntity): """Return True if entity is on.""" return self._async_detach_triggers is not None - @asyncio.coroutine - def async_added_to_hass(self) -> None: + async def async_added_to_hass(self) -> None: """Startup with initial state or previous state.""" if self._initial_state is not None: enable_automation = self._initial_state _LOGGER.debug("Automation %s initial state %s from config " "initial_state", self.entity_id, enable_automation) else: - state = yield from async_get_last_state(self.hass, self.entity_id) + state = await async_get_last_state(self.hass, self.entity_id) if state: enable_automation = state.state == STATE_ON self._last_triggered = state.attributes.get('last_triggered') @@ -298,54 +294,50 @@ class AutomationEntity(ToggleEntity): # HomeAssistant is starting up if self.hass.state == CoreState.not_running: - @asyncio.coroutine - def async_enable_automation(event): + async def async_enable_automation(event): """Start automation on startup.""" - yield from self.async_enable() + await self.async_enable() self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_START, async_enable_automation) # HomeAssistant is running else: - yield from self.async_enable() + await self.async_enable() - @asyncio.coroutine - def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs) -> None: """Turn the entity on and update the state.""" if self.is_on: return - yield from self.async_enable() + await self.async_enable() - @asyncio.coroutine - def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs) -> None: """Turn the entity off.""" if not self.is_on: return self._async_detach_triggers() self._async_detach_triggers = None - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_trigger(self, variables, skip_condition=False): + async def async_trigger(self, variables, skip_condition=False, + context=None): """Trigger automation. This method is a coroutine. """ if skip_condition or self._cond_func(variables): - yield from self._async_action(self.entity_id, variables) + self.async_set_context(context) + await self._async_action(self.entity_id, variables, context) self._last_triggered = utcnow() - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self): """Remove listeners when removing automation from HASS.""" - yield from self.async_turn_off() + await self.async_turn_off() - @asyncio.coroutine - def async_enable(self): + async def async_enable(self): """Enable this automation entity. This method is a coroutine. @@ -353,9 +345,9 @@ class AutomationEntity(ToggleEntity): if self.is_on: return - self._async_detach_triggers = yield from self._async_attach_triggers( + self._async_detach_triggers = await self._async_attach_triggers( self.async_trigger) - yield from self.async_update_ha_state() + await self.async_update_ha_state() @property def device_state_attributes(self): @@ -368,8 +360,7 @@ class AutomationEntity(ToggleEntity): } -@asyncio.coroutine -def _async_process_config(hass, config, component): +async def _async_process_config(hass, config, component): """Process config and add automations. This method is a coroutine. @@ -411,20 +402,19 @@ def _async_process_config(hass, config, component): entities.append(entity) if entities: - yield from component.async_add_entities(entities) + await component.async_add_entities(entities) def _async_get_action(hass, config, name): """Return an action based on a configuration.""" script_obj = script.Script(hass, config, name) - @asyncio.coroutine - def action(entity_id, variables): + async def action(entity_id, variables, context): """Execute an action.""" _LOGGER.info('Executing %s', name) logbook.async_log_entry( hass, name, 'has been triggered', DOMAIN, entity_id) - yield from script_obj.async_run(variables) + await script_obj.async_run(variables, context) return action @@ -448,8 +438,7 @@ def _async_process_if(hass, config, p_config): return if_action -@asyncio.coroutine -def _async_process_trigger(hass, config, trigger_configs, name, action): +async def _async_process_trigger(hass, config, trigger_configs, name, action): """Set up the triggers. This method is a coroutine. @@ -457,13 +446,13 @@ def _async_process_trigger(hass, config, trigger_configs, name, action): removes = [] for conf in trigger_configs: - platform = yield from async_prepare_setup_platform( + platform = await async_prepare_setup_platform( hass, config, DOMAIN, conf.get(CONF_PLATFORM)) if platform is None: return None - remove = yield from platform.async_trigger(hass, conf, action) + remove = await platform.async_trigger(hass, conf, action) if not remove: _LOGGER.error("Error setting up trigger %s", name) diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index 7c035d7d1a5..e19a85edae6 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -45,11 +45,11 @@ def async_trigger(hass, config, action): # If event data doesn't match requested schema, skip event return - hass.async_run_job(action, { + hass.async_run_job(action({ 'trigger': { 'platform': 'event', 'event': event, }, - }) + }, context=event.context)) return hass.bus.async_listen(event_type, handle_event) diff --git a/homeassistant/components/automation/homeassistant.py b/homeassistant/components/automation/homeassistant.py index 74cf195bc61..b55d99f706a 100644 --- a/homeassistant/components/automation/homeassistant.py +++ b/homeassistant/components/automation/homeassistant.py @@ -32,12 +32,12 @@ def async_trigger(hass, config, action): @callback def hass_shutdown(event): """Execute when Home Assistant is shutting down.""" - hass.async_run_job(action, { + hass.async_run_job(action({ 'trigger': { 'platform': 'homeassistant', 'event': event, }, - }) + }, context=event.context)) return hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, hass_shutdown) @@ -45,11 +45,11 @@ def async_trigger(hass, config, action): # Automation are enabled while hass is starting up, fire right away # Check state because a config reload shouldn't trigger it. if hass.state == CoreState.starting: - hass.async_run_job(action, { + hass.async_run_job(action({ 'trigger': { 'platform': 'homeassistant', 'event': event, }, - }) + })) return lambda: None diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index b59271f25e5..f0dcbf0be57 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -66,7 +66,7 @@ def async_trigger(hass, config, action): @callback def call_action(): """Call action with right context.""" - hass.async_run_job(action, { + hass.async_run_job(action({ 'trigger': { 'platform': 'numeric_state', 'entity_id': entity, @@ -75,7 +75,7 @@ def async_trigger(hass, config, action): 'from_state': from_s, 'to_state': to_s, } - }) + }, context=to_s.context)) matching = check_numeric_state(entity, from_s, to_s) diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 9243f960850..263d4158e25 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -43,7 +43,7 @@ def async_trigger(hass, config, action): @callback def call_action(): """Call action with right context.""" - hass.async_run_job(action, { + hass.async_run_job(action({ 'trigger': { 'platform': 'state', 'entity_id': entity, @@ -51,7 +51,7 @@ def async_trigger(hass, config, action): 'to_state': to_s, 'for': time_delta, } - }) + }, context=to_s.context)) # Ignore changes to state attributes if from/to is in use if (not match_all and from_s is not None and to_s is not None and diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index 0fcdeaae5e0..67a44f1a347 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -32,13 +32,13 @@ def async_trigger(hass, config, action): @callback def template_listener(entity_id, from_s, to_s): """Listen for state changes and calls action.""" - hass.async_run_job(action, { + hass.async_run_job(action({ 'trigger': { 'platform': 'template', 'entity_id': entity_id, 'from_state': from_s, 'to_state': to_s, }, - }) + }, context=to_s.context)) return async_track_template(hass, value_template, template_listener) diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index 61d846582cb..f30dfe753cb 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -51,7 +51,7 @@ def async_trigger(hass, config, action): # pylint: disable=too-many-boolean-expressions if event == EVENT_ENTER and not from_match and to_match or \ event == EVENT_LEAVE and from_match and not to_match: - hass.async_run_job(action, { + hass.async_run_job(action({ 'trigger': { 'platform': 'zone', 'entity_id': entity, @@ -60,7 +60,7 @@ def async_trigger(hass, config, action): 'zone': zone_state, 'event': event, }, - }) + }, context=to_s.context)) return async_track_state_change(hass, entity_id, zone_automation_listener, MATCH_ALL, MATCH_ALL) diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index a45f8ba8930..247ac07283e 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -63,11 +63,11 @@ def is_on(hass, entity_id): @bind_hass -def turn_on(hass, entity_id, variables=None): +def turn_on(hass, entity_id, variables=None, context=None): """Turn script on.""" _, object_id = split_entity_id(entity_id) - hass.services.call(DOMAIN, object_id, variables) + hass.services.call(DOMAIN, object_id, variables, context=context) @bind_hass @@ -97,45 +97,41 @@ def async_reload(hass): return hass.services.async_call(DOMAIN, SERVICE_RELOAD) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Load the scripts from the configuration.""" component = EntityComponent( _LOGGER, DOMAIN, hass, group_name=GROUP_NAME_ALL_SCRIPTS) - yield from _async_process_config(hass, config, component) + await _async_process_config(hass, config, component) - @asyncio.coroutine - def reload_service(service): + async def reload_service(service): """Call a service to reload scripts.""" - conf = yield from component.async_prepare_reload() + conf = await component.async_prepare_reload() if conf is None: return - yield from _async_process_config(hass, conf, component) + await _async_process_config(hass, conf, component) - @asyncio.coroutine - def turn_on_service(service): + async def turn_on_service(service): """Call a service to turn script on.""" # We could turn on script directly here, but we only want to offer # one way to do it. Otherwise no easy way to detect invocations. var = service.data.get(ATTR_VARIABLES) for script in component.async_extract_from_service(service): - yield from hass.services.async_call(DOMAIN, script.object_id, var) + await hass.services.async_call(DOMAIN, script.object_id, var, + context=service.context) - @asyncio.coroutine - def turn_off_service(service): + async def turn_off_service(service): """Cancel a script.""" # Stopping a script is ok to be done in parallel - yield from asyncio.wait( + await asyncio.wait( [script.async_turn_off() for script in component.async_extract_from_service(service)], loop=hass.loop) - @asyncio.coroutine - def toggle_service(service): + async def toggle_service(service): """Toggle a script.""" for script in component.async_extract_from_service(service): - yield from script.async_toggle() + await script.async_toggle(context=service.context) hass.services.async_register(DOMAIN, SERVICE_RELOAD, reload_service, schema=RELOAD_SERVICE_SCHEMA) @@ -149,18 +145,17 @@ def async_setup(hass, config): return True -@asyncio.coroutine -def _async_process_config(hass, config, component): - """Process group configuration.""" - @asyncio.coroutine - def service_handler(service): +async def _async_process_config(hass, config, component): + """Process script configuration.""" + async def service_handler(service): """Execute a service call to script.