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 | 44 +++--- .../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, 388 insertions(+), 38 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 b5ba869cdf13c..e0b7b377b1fc7 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -6,8 +6,6 @@ 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 @@ async def async_enable_user_mfa(self, user: models.User, 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 d0707c4a7452f..cb0758e3ef8c0 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -8,7 +8,7 @@ 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 @@ def input_schema(self) -> vol.Schema: """Return a voluptuous schema to define mfa auth module's input.""" raise NotImplementedError - @property - def setup_schema(self) -> Optional[vol.Schema]: - """Return a vol schema to validate mfa auth module's setup input. + 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 @@ async def async_validation( 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 59b3f64d2e052..9c72111ef9697 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 @@ def input_schema(self) -> vol.Schema: 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 4251b23e51475..a87e646761c47 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -68,10 +68,12 @@ 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 retrieve_result(client_id, result_type, code): 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') - - if user is None: - connection.to_write.put_nowait(websocket_api.error_message( - msg['id'], 'no_user', 'Not authenticated as a user')) - return - - 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] - })) + async def async_get_current_user(user): + """Get current user.""" + enabled_modules = await hass.auth.async_get_enabled_mfa(user) + + 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], + })) + + 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 0000000000000..82eb913d89082 --- /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 1ba0e20d55345..0c9ab366534f2 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -18,7 +18,7 @@ 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 9d90532728afa..e6f83762cd770 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 0000000000000..93b5cdf7bb919 --- /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 d3d27c053335c..9aa0c446f2bdf 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -7,9 +7,10 @@ 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 @@ def device_state_attributes(self): 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 cf8d891661ec2..d435e9e3c04b4 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -12,6 +12,7 @@ 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 @@ 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 @@ def async_add_remote(sensors): 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 e7bc5605aee40..e629d57f2017f 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -8,6 +8,7 @@ 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 6dce6b7fdb854..067f1474f962d 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -6,13 +6,14 @@ """ 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 @@ def device_state_attributes(self): 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 a32f1e5e21062..45c604a74ee6a 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -6,10 +6,11 @@ """ 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 @@ def device_state_attributes(self): 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 @@ def device_state_attributes(self): 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 11f7f42c6c927..7d861e4c29cf4 100644 --- a/homeassistant/components/switch/deconz.py +++ b/homeassistant/components/switch/deconz.py @@ -6,9 +6,10 @@ """ 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 @@ def should_poll(self): """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 3276763a96769..19a6eaa62dc2f 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -15,15 +15,18 @@ 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 @@ def async_get_device(self, identifiers: str, connections: tuple): 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 @@ def async_get_or_create(self, identifiers, manufacturer, model, return device device = DeviceEntry( + connection=connection, identifiers=identifiers, manufacturer=manufacturer, model=model, - connection=connection, name=name, sw_version=sw_version ) @@ -93,10 +96,10 @@ def _data_to_save(self): 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 c65aa5e98c209..ffac68c5f07e7 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -275,8 +275,11 @@ async def _async_add_entity(self, entity, update_before_add, 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 25480a023ec0f..447c6348500ef 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 71cbc724c5997..52688beaa2688 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 c6fc130a4a41a..049a3b961b610 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -7,6 +7,16 @@ 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 41e7d39e977a0..f7792eb52504b 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 bbd863b5693dd..90098a677a147 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -92,7 +92,7 @@ def validator(value: Any) -> str: 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 9e85e85818d09..3df285863139a 100644 --- a/homeassistant/components/konnected.py +++ b/homeassistant/components/konnected.py @@ -16,7 +16,7 @@ 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 @@ 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 @@ def device_discovered(service, info): 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 @@ def save_data(self): _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 @@ def actuator_configuration(self): 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 @@ def sync_device_config(self): 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 @@ async def put(self, request: Request, device_id, 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 c085d0bb0a5de..20774accbd579 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 @@ def __init__(self, device_id, pin_num, data, client): 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 @@ def is_on(self): """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 @@ def turn_on(self, **kwargs): 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 7ce51454ee5f6..a6c602602f4ae 100644 --- a/homeassistant/components/sensor/darksky.py +++ b/homeassistant/components/sensor/darksky.py @@ -33,10 +33,12 @@ 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 @@ 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 @@ def update(self): '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 @@ def _update(self): 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 d01d1b50c5acc..7adcc43f4af26 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -30,8 +30,10 @@ async def auth_middleware(request, handler): 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 9aa0c446f2bdf..1fb6212440715 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -116,15 +116,15 @@ def device_state_attributes(self): 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 d435e9e3c04b4..a4edc009ea15d 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -123,8 +123,9 @@ def async_add_remote(sensors): 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 067f1474f962d..412cf8693e59a 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -202,15 +202,15 @@ def device_state_attributes(self): 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 fa1120db98c84..fca7b29d2ecc7 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -87,7 +87,7 @@ def __init__(self, host): self.ip_address = host self.channels = [] self.current_app = None - self.device_info = {} + self._device_info = {} self.update() @@ -96,7 +96,7 @@ def update(self): 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 @@ def should_poll(self): @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 @@ def supported_features(self): @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 f2ac45a996f9c..489d028aad46b 100644 --- a/homeassistant/components/media_player/soundtouch.py +++ b/homeassistant/components/media_player/soundtouch.py @@ -166,6 +166,11 @@ def config(self): """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 @@ def create_zone(self, slaves): _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 @@ def remove_zone_slave(self, slaves): _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 @@ def add_zone_slave(self, slaves): _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 45c604a74ee6a..8cb3915dc46e8 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -136,15 +136,15 @@ def device_state_attributes(self): 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 @@ def device_state_attributes(self): 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 7d861e4c29cf4..35dbc3ef782e3 100644 --- a/homeassistant/components/switch/deconz.py +++ b/homeassistant/components/switch/deconz.py @@ -81,15 +81,15 @@ def should_poll(self): 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 19a6eaa62dc2f..31da40134a558 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -23,8 +23,9 @@ 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 @@ def async_get_device(self, identifiers: str, connections: tuple): """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 @@ async def async_load(self): 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 @@ def _data_to_save(self): 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 78806e65ef15b..695da5bce9c7d 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -131,7 +131,7 @@ def device_state_attributes(self): 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 ffac68c5f07e7..083a2946122fc 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -272,15 +272,16 @@ async def _async_add_entity(self, entity, update_before_add, 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 f7792eb52504b..b2e7307182397 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 89649ecb8e174..8ebacc3736b6b 100644 --- a/homeassistant/components/hangouts/__init__.py +++ b/homeassistant/components/hangouts/__init__.py @@ -26,8 +26,8 @@ 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 bd81d5053c84c..74eb14b050da3 100644 --- a/homeassistant/components/hangouts/config_flow.py +++ b/homeassistant/components/hangouts/config_flow.py @@ -104,4 +104,4 @@ async def async_step_final(self): 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 41fa61964de81..c10964e2da34d 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 45505bbbc9b13..fe8f8ef0f6008 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 77a30fd771b35..76ea576ac28b4 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 bfcf7322749c8..c475ea5597431 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ 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 447c6348500ef..a5cfe9e402a59 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 52688beaa2688..7b9dc1d1eb34e 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 471c1c6e82cad..5c04554445676 100644 --- a/homeassistant/components/zoneminder.py +++ b/homeassistant/components/zoneminder.py @@ -11,16 +11,19 @@ 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 @@ 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 8c9449169058a..a6cdcc7cf9025 100644 --- a/homeassistant/components/velbus.py +++ b/homeassistant/components/velbus.py @@ -12,7 +12,7 @@ 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 a5cfe9e402a59..c150ac482bf43 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 19bacbc8d4c2c..71be9c2435e1b 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -550,6 +550,7 @@ async def async_publish(self, topic: str, payload: PublishPayloadType, 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 527b8c8f0186f..53c8e26701615 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -869,7 +869,7 @@ def _hm_event_callback(self, device, caller, attribute, value): # 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 f3d8e269a42a0..47d6e181c8f82 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -35,7 +35,7 @@ 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 a2e9549a117e7..53821275d42de 100644 --- a/homeassistant/components/sensor/sql.py +++ b/homeassistant/components/sensor/sql.py @@ -20,7 +20,7 @@ _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 c150ac482bf43..41d716c28f137 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 7b9dc1d1eb34e..af68edbd632da 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 eafc6fdf61657..a59eb70549880 100644 --- a/homeassistant/components/sensor/tahoma.py +++ b/homeassistant/components/sensor/tahoma.py @@ -11,12 +11,15 @@ 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 @@ def update(self): 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 a7d1d597f67c8..d0707b0f067be 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 @@ def async_register_callbacks(self): 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 @@ def should_poll(self): @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 c952e7c8987a9..c60463a8663e6 100644 --- a/homeassistant/components/binary_sensor/nest.py +++ b/homeassistant/components/binary_sensor/nest.py @@ -130,7 +130,7 @@ def device_class(self): 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 @@ def device_class(self): 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 c412ec37e517a..450a5e580bdc0 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 175dbcd2267b3..e1d26371984a8 100644 --- a/homeassistant/components/camera/nest.py +++ b/homeassistant/components/camera/nest.py @@ -46,7 +46,7 @@ def __init__(self, structure, device): """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 @@ def turn_off(self): # 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 @@ def turn_on(self): # 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 @@ def camera_image(self): """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 ed197f57ab3fe..4eada356653fc 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 @@ def __init__(self, hass, device): 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 @@ def async_register_callbacks(self): 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 @@ def temperature_unit(self): @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 @@ def operation_list(self): """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 81c5fb3c2aa14..321559f10eefc 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -57,7 +57,7 @@ def __init__(self, structure, device, temp_unit): """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 @@ def __init__(self, structure, device, temp_unit): 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 @@ def __init__(self, structure, device, temp_unit): 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 @@ def supported_features(self): @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 @@ def set_temperature(self, **kwargs): _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 @@ def set_operation_mode(self, operation_mode): _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 @@ def fan_list(self): 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 @@ def max_temp(self): 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 3d1d8e6a53efa..429b544aefc40 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 @@ def __init__(self, device, hold_temp, away_temps): # 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 @@ def set_fan_mode(self, fan_mode): """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 @@ def update(self): # 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 @@ def set_temperature(self, **kwargs): 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 @@ def set_time(self): 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 74ac80a476dfd..43a87fab36736 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 @@ def async_register_callbacks(self): 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 @@ def supported_features(self): """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 @@ def stop_auto_updater(self): 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 fc352aa8482e3..9d292d9e8b5bb 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 2ed32521f1dc4..55567d4587901 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 23929db8626b9..778d2fac59c15 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 @@ def async_register_callbacks(self): 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 @@ def should_poll(self): @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 @@ def effect(self): @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 fa986ff09f20a..413358d9cee07 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 6f39fb3b318a8..07b5458fa4506 100644 --- a/homeassistant/components/light/tellduslive.py +++ b/homeassistant/components/light/tellduslive.py @@ -38,7 +38,7 @@ def changed(self): @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 @@ def supported_features(self): @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 b64aad38b3e81..809db228d0262 100644 --- a/homeassistant/components/media_player/emby.py +++ b/homeassistant/components/media_player/emby.py @@ -133,7 +133,7 @@ def __init__(self, emby, device_id): _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 @@ def async_added_to_hass(self): 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 @@ def unique_id(self): @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 @@ def should_poll(self): @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 @@ def state(self): 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 @@ def media_content_type(self): @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 @@ def media_position_updated_at(self): @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 @@ def async_media_play(self): 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 3c916860818cd..35906cf502388 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -454,7 +454,7 @@ def _set_player_state(self): 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 @@ def app_name(self): """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 @@ def supported_features(self): 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 @@ def supported_features(self): 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 @@ def mute_volume(self, mute): - 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 @@ def mute_volume(self, mute): 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 @@ def turn_off(self): 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 @@ def play_media(self, media_type, media_id, **kwargs): 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 @@ def play_media(self, media_type, media_id, **kwargs): 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 @@ def _get_tv_media(self, library_name, show_name, season_number, 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 @@ def _get_tv_media(self, library_name, show_name, season_number, 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 @@ def _get_tv_media(self, library_name, show_name, season_number, 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 489d028aad46b..4e26af9dcc2fd 100644 --- a/homeassistant/components/media_player/soundtouch.py +++ b/homeassistant/components/media_player/soundtouch.py @@ -323,8 +323,8 @@ def create_zone(self, slaves): _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 @@ def remove_zone_slave(self, slaves): _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 @@ def add_zone_slave(self, slaves): _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 04163f1ca1361..57111350396e1 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -282,12 +282,12 @@ def __init__(self, structure, device, variable): 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 f9a6a4b25f219..750e39455696e 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 @@ async def async_send_message(self, message="", **kwargs): 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 8af0e8db28d10..63e30a9491ede 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 7fbcba5a26ea6..723f575ba349a 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -188,6 +188,11 @@ def name(self): """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 @@ def is_on(self): """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 @@ def _send_command(self, payload): _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 b8ef38981e82a..18725394a1f50 100644 --- a/homeassistant/components/sensor/juicenet.py +++ b/homeassistant/components/sensor/juicenet.py @@ -49,14 +49,14 @@ def __init__(self, device, sensor_type, hass): @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 @@ def state(self): """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 @@ def device_state_attributes(self): """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 b8b55a1cc7ceb..ec506189c1282 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 @@ def async_register_callbacks(self): 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 @@ def should_poll(self): @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 d51b0ab405332..738bc53d8806a 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -140,15 +140,15 @@ def update(self): 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 @@ def device_class(self): 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 c3cc75dac0ce1..55928a80f136e 100644 --- a/homeassistant/components/sensor/tank_utility.py +++ b/homeassistant/components/sensor/tank_utility.py @@ -79,10 +79,15 @@ def __init__(self, email, password, token, device): 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 @@ def get_data(self): 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 3490859595174..4676e08a24741 100644 --- a/homeassistant/components/sensor/tellduslive.py +++ b/homeassistant/components/sensor/tellduslive.py @@ -67,7 +67,7 @@ def _type(self): @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 af60cee127a62..678a8d4775f46 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 @@ def async_register_callbacks(self): 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 @@ def should_poll(self): @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 c1134fc21c1f5..0263dfd8198c9 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 58be267bbbcbb..693499510adcc 100644 --- a/homeassistant/components/tellduslive.py +++ b/homeassistant/components/tellduslive.py @@ -287,14 +287,14 @@ def __init__(self, hass, device_id): 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 @@ def device_id(self): """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 @@ def _battery_level(self): 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 ac681dc691a62..3027b6f8ca67b 100644 --- a/homeassistant/components/sensor/dnsip.py +++ b/homeassistant/components/sensor/dnsip.py @@ -19,11 +19,13 @@ _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 @@ 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 @@ @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 @@ def state(self): @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 | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index d4c5606799d9b..d9ffb4cbace7d 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -195,23 +195,15 @@ async def _async_list_conversations(self): import hangups self._user_list, self._conversation_list = \ (await hangups.build_user_conversation_list(self._client)) - users = {} conversations = {} - for user in self._user_list.get_all(): - users[str(user.id_.chat_id)] = {'full_name': user.full_name, - 'is_self': user.is_self} - - for conv in self._conversation_list.get_all(): - users_in_conversation = {} + for 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} - - self.hass.states.async_set("{}.users".format(DOMAIN), - len(self._user_list.get_all()), - attributes=users) + 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("{}.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 a19efcfb1afff..6c245ffdf434d 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 c475ea5597431..4622f80948ec2 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ 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 41d716c28f137..2cd19d58ce609 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 af68edbd632da..69a02d5900dd2 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 26628d7fe6255..fdff380301fc2 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 2cd19d58ce609..2f27662b2e72e 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 7484dc286e62e..8e2ad008cc6e8 100755 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ 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 e0b7b377b1fc7..952bb3b8352cc 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -249,13 +249,13 @@ async def async_disable_user_mfa(self, user: models.User, 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 0000000000000..48531863c1a35 --- /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 e8ef7cbf3d4f8..0bcb47d4af9e2 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -168,7 +168,7 @@ def __init__(self, auth_provider: AuthProvider) -> None: 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 @@ async def async_step_select_mfa_module( 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 0000000000000..5c1af67b120ce --- /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 0000000000000..b0083ab577b4c --- /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 fe8f8ef0f6008..a799094c94d31 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 2f27662b2e72e..691bbf62246d6 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 69a02d5900dd2..5fa4af21a62cd 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 e26393bb800c5..fe23e638e5b76 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -78,6 +78,7 @@ '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 0000000000000..28e6c949bc4ad --- /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 76ea576ac28b4..3cfe67f70b107 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -16,7 +16,7 @@ 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 ce252497901a0..c743a5b7f6568 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -5,13 +5,16 @@ 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 @@ def validate_login(self, username: str, password: str) -> None: 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 @@ def validate_login(self, username: str, password: str) -> None: 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 fdff380301fc2..70fb519eef4ee 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 691bbf62246d6..e1b5b1c70ec8f 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 8e2ad008cc6e8..b1b0af70319c2 100755 --- a/setup.py +++ b/setup.py @@ -36,6 +36,7 @@ '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 c92f8539b1708..935c5e50dd5e6 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 eb278afaf7f05..6e70a1f431069 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 9cbc02f126efd..a8314761f8d39 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 730d9404837ea..c3363215201f0 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 6e5af1c26cc97..d4ecbe5010725 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 d8c6cff9b0cc6..9340070d9a39c 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 5c6e409245c7e..b30764c92393f 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 0000000000000..ea4af320addd5 --- /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 a6cdcc7cf9025..d2def6f96bc7a 100644 --- a/homeassistant/components/velbus.py +++ b/homeassistant/components/velbus.py @@ -12,7 +12,7 @@ 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 @@ def callback(): 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 @@ def callback(): 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 e1b5b1c70ec8f..959fd21732fe3 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 a8ce6917dd392..433bb8e9ed175 100644 --- a/homeassistant/components/sensor/trafikverket_weatherstation.py +++ b/homeassistant/components/sensor/trafikverket_weatherstation.py @@ -4,119 +4,115 @@ 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 + + sensor_name = config[CONF_NAME] + sensor_api = config[CONF_API_KEY] + sensor_station = config[CONF_STATION] + + web_session = async_get_clientsession(hass) - add_entities([TrafikverketWeatherStation( - sensor_name, sensor_api, sensor_station, sensor_type)], True) + 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. - 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. + async def async_update(self): + """Get the latest data from Trafikverket and updates the states.""" 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 959fd21732fe3..92aeec6aefeb7 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 2ca519d225c4a..217df0aacd47b 100644 --- a/homeassistant/components/device_tracker/bluetooth_tracker.py +++ b/homeassistant/components/device_tracker/bluetooth_tracker.py @@ -12,7 +12,8 @@ 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 discover_devices(): 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 update_bluetooth(now): 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 2c7325866acf9..7d215fb6bafb9 100644 --- a/homeassistant/components/sensor/geizhals.py +++ b/homeassistant/components/sensor/geizhals.py @@ -13,15 +13,15 @@ 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 @@ 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 @@ def icon(self): @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 92aeec6aefeb7..2a238a933b8f9 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 0000000000000..0c609b3430ac3 --- /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 0000000000000..7ea074470c76d --- /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 0000000000000..4dffe492c4d84 --- /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 0000000000000..0920e0325d20e --- /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 0000000000000..2566eb2557075 --- /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 0000000000000..fce5152b3f909 --- /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 0000000000000..af4cefff6e5a3 --- /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 0000000000000..1b3b25dbcff66 --- /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 5c1af67b120ce..a0fd20e9d083b 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 ac08c26229ce2..6909a0e46640d 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -200,18 +200,10 @@ def __init__(self, hass, api_password, 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 7adcc43f4af26..a18b4de7a1078 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -32,7 +32,7 @@ async def auth_middleware(request, handler): 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 bb0be2d94336e..449883265f6f2 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 0000000000000..47aa580e3d4ef --- /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 2a238a933b8f9..81de7219b60d1 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 b9e403710f104..38669f8175baa 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 693499510adcc..c2b7ba9ba0f53 100644 --- a/homeassistant/components/tellduslive.py +++ b/homeassistant/components/tellduslive.py @@ -287,7 +287,6 @@ def __init__(self, hass, device_id): 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 c0b280d2d69c5..79943a8b08472 100644 --- a/homeassistant/components/sensor/alpha_vantage.py +++ b/homeassistant/components/sensor/alpha_vantage.py @@ -15,7 +15,7 @@ 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 81de7219b60d1..42fdb32185703 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 4622f80948ec2..f0976c7822482 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ 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 42fdb32185703..85af517bb2968 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 5fa4af21a62cd..5dcf0550aba77 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 | 182 ++++++++++-------- tests/components/test_conversation.py | 12 +- 6 files changed, 200 insertions(+), 146 deletions(-) create mode 100644 homeassistant/components/conversation/util.py diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index 9cb00a84583ae..d8d386f5ca048 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -11,6 +11,7 @@ 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 @@ def component_loaded(event): 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 0000000000000..60d861afdbe4a --- /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 8ebacc3736b6b..72a7e015a2248 100644 --- a/homeassistant/components/hangouts/__init__.py +++ b/homeassistant/components/hangouts/__init__.py @@ -11,28 +11,56 @@ 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 + + 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)} - if configured_hangouts(hass) is None: - hass.async_add_job(hass.config_entries.flow.async_init( - DOMAIN, context={'source': config_entries.SOURCE_IMPORT} - )) + 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 7083307f3e22e..3b96edf93a29a 100644 --- a/homeassistant/components/hangouts/const.py +++ b/homeassistant/components/hangouts/const.py @@ -4,7 +4,6 @@ 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_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 @@ 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 d9ffb4cbace7d..15f4156d37448 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 @@ 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 @@ def _resolve_conversation_name(self, name): 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 command['_' + CONF_CONVERSATIONS]: - if conv_id not in self._expression_commands: - self._expression_commands[conv_id] = [] - self._expression_commands[conv_id].append(command) + for conv_id in data['_' + CONF_CONVERSATIONS]: + if conv_id not in self._conversation_intents: + self._conversation_intents[conv_id] = {} + + 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): - from hangups import ChatMessageEvent - if event.__class__ is ChatMessageEvent: - self._handle_conversation_message( - event.conversation_id, event.user_id, 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) - def _handle_conversation_message(self, conv_id, user_id, event): + async def _async_handle_conversation_event(self, event): + from hangups import ChatMessageEvent + if isinstance(event, ChatMessageEvent): + dispatcher.async_dispatcher_send(self.hass, + EVENT_HANGOUTS_MESSAGE_RECEIVED, + event.conversation_id, + event.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) - - event_data = None - - pieces = event.text.split(' ') - cmd = pieces[0].lower() - command = self._word_commands.get(conv_id, {}).get(cmd) - if command: - event_data = { - 'command': command[CONF_NAME], - 'conversation_id': conv_id, - 'user_id': user_id, - 'user_name': user.full_name, - 'data': pieces[1:] - } - else: - # After single-word commands, check all regex commands in the room - for command in self._expression_commands.get(conv_id, []): - match = command['_' + CONF_EXPRESSION].match(event.text) + message, user.full_name) + + 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) + 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 @@ async def _async_send_message(self, message, targets): conversations = [] for target in targets: conversation = None - if 'id' in target: - conversation = self._conversation_list.get(target['id']) - elif 'name' in target: - conversation = self._resolve_conversation_name(target['name']) + if 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 @@ async def _async_list_conversations(self): 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 6a1d5a55c47eb..61247b5bdde1d 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 31da40134a558..504448b948df3 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 @@ def __init__(self, hass): @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 @@ def async_get_or_create(self, *, config_entry, connections, identifiers, name=name, sw_version=sw_version ) - self.devices.append(device) + self.devices[device.id] = device self.async_schedule_save() @@ -86,10 +88,10 @@ async def async_load(self): 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 @@ async def async_load(self): 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 @@ def _data_to_save(self): '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 b2e7307182397..84ad54f7b829b 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 37e032e58d7ab..8a7e1d67c6d25 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -111,31 +111,19 @@ async def async_step_init( 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 4839c72a86a7c..0ca302f827305 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 6909a0e46640d..1b22f8e62d431 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -202,7 +202,7 @@ def __init__(self, hass, api_password, 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 feefa65c0f624..3f12fc223b817 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 19e85a094ee16..1e93a078bd925 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 cb0758e3ef8c0..a669f8bb5f0fc 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 0bcb47d4af9e2..d8ec04e907256 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 70fb519eef4ee..3e9a763181a2c 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 fe23e638e5b76..4b694ec7ec071 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -124,6 +124,8 @@ 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 | 82 +++++++++---------------------- tests/test_requirements.py | 92 +++++++++++++++++++++++++++++------ tests/util/test_package.py | 78 +++-------------------------- 4 files changed, 167 insertions(+), 145 deletions(-) diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index b9b5e137d5c10..a3d168d22e749 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -3,12 +3,17 @@ 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 3f12fc223b817..422809f759439 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -4,17 +4,11 @@ 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 - - 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 + _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 async def async_get_user_site(deps_dir: str) -> str: diff --git a/tests/test_requirements.py b/tests/test_requirements.py index e3ef797df4d98..71ae80f22e49b 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 @@ def teardown_method(self, method): """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 @@ def test_requirement_installed_in_venv( 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 @@ def test_requirement_installed_in_deps( 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 1e93a078bd925..5422140c232b6 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -6,18 +6,12 @@ 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 communicate(input=None): 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 72a7e015a2248..ebadff57be3b1 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 952bb3b8352cc..4ef8440de62a2 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 a669f8bb5f0fc..603ca6ff3b16d 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -11,6 +11,7 @@ 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 @@ async def async_step_init( 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 d8ec04e907256..370391d57cdd2 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -10,6 +10,7 @@ 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 @@ async def async_user_meta_for_credentials( 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 c10964e2da34d..2051359c0baed 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 a799094c94d31..d742e62660b90 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -8,7 +8,7 @@ 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 @@ 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 @@ """ +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 @@ 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 935c5e50dd5e6..84beb8cdd3f47 100644 --- a/tests/auth/providers/test_homeassistant.py +++ b/tests/auth/providers/test_homeassistant.py @@ -3,6 +3,7 @@ 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 f724b40a71f16..d9e7a50410f72 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -3,6 +3,7 @@ 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 3cfe67f70b107..e4a6798093ffb 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 0000000000000..f55ae4b97ba0e --- /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 0000000000000..c5b397a8e12a6 --- /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 0000000000000..ef41ea8724811 --- /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 0000000000000..426ab68962619 --- /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 0000000000000..bee6bf753dbb5 --- /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 0920e0325d20e..16234acb193e6 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 38970e4a97cad..930b649bceb12 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 35906cf502388..46dacd98aadd5 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -531,7 +531,7 @@ def app_name(self): @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 48531863c1a35..0914658a6557e 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -137,8 +137,9 @@ async def async_validation( 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 370391d57cdd2..3cb1c6b121e4c 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -224,19 +224,27 @@ async def async_step_mfa( 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 e6f83762cd770..80109627140d5 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 28e6c949bc4ad..6e3558ec5496e 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 d9e7a50410f72..63b2b4408dd8a 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 f0976c7822482..0156a8b2cd6c9 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ 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 85af517bb2968..5868e6df5adfc 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 5dcf0550aba77..94c35c30ddfc9 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 0000000000000..726fa6a6cd1ed --- /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 0000000000000..b4b5b58f9fa7c --- /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 0000000000000..aabf977a8cc57 --- /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 d72bde548d3de..3bb468c1b1eee 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 449883265f6f2..0c4a1f7d569fe 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 0000000000000..6ce4421ebc858 --- /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 5868e6df5adfc..16c670167809a 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 bedc041fcccfa..6a17345188a4d 100644 --- a/homeassistant/components/cover/myq.py +++ b/homeassistant/components/cover/myq.py @@ -8,17 +8,25 @@ 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 @@ def name(self): @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 @@ def open_cover(self, **kwargs): """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 16c670167809a..e31a2349dcfd7 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 5d7855f395982..2bf0572d498bd 100644 --- a/homeassistant/components/upnp.py +++ b/homeassistant/components/upnp.py @@ -15,7 +15,7 @@ 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 e31a2349dcfd7..ba0da439cbd44 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 94c35c30ddfc9..2415d661e29b0 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 7e54586b81008..dd421fee57a9c 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 ae9589c7886a5..2954e427ed543 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 @@ async def async_set_cast_info(self, cast_info): # 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 @@ def name(self): """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 8fe285a59cd74..7345fd0c1582a 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 c4309519e36df..4fc6b8b09540d 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -388,6 +388,18 @@ def name(self): """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 321559f10eefc..f81736b3a520d 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -8,7 +8,8 @@ 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 @@ def unique_id(self): """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 c04380e13035c..38b521078f422 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -11,7 +11,8 @@ 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 2a51423a7a815..6f6e0ed617e01 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -285,6 +285,25 @@ def effect_list(self): """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 d12270cd908dc..1c4768746d5b6 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 f81736b3a520d..bc63512fcf3b7 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -138,6 +138,7 @@ def device_info(self): '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 0c4a1f7d569fe..39c31e4e40b0b 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 0000000000000..44b9e39215785 --- /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 0000000000000..a063b1577f5cc --- /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 0000000000000..d2f13eb30e694 --- /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 ba0da439cbd44..ef89fb096daa3 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 c42090e3b7afe..730b662b90b8a 100644 --- a/homeassistant/components/binary_sensor/xiaomi_aqara.py +++ b/homeassistant/components/binary_sensor/xiaomi_aqara.py @@ -374,11 +374,11 @@ def __init__(self, device, hass, xiaomi_hub): 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 0156a8b2cd6c9..da3d225bba088 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ 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 ef89fb096daa3..c788a2428349c 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 2415d661e29b0..8e10876cc063f 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 a6d8fccdee033..41842459c8a79 100644 --- a/homeassistant/components/vacuum/xiaomi_miio.py +++ b/homeassistant/components/vacuum/xiaomi_miio.py @@ -87,6 +87,7 @@ 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 1fb6212440715..d2ca9e7c5e880 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -54,6 +54,11 @@ async def async_added_to_hass(self): 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 a4edc009ea15d..e9f797d95f96f 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -24,7 +24,7 @@ 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 @@ def __init__(self, hass, device): 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 412cf8693e59a..ff3fe60992433 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -82,6 +82,11 @@ async def async_added_to_hass(self): 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 dde78dadc49f7..5af8f657206cb 100644 --- a/homeassistant/components/scene/deconz.py +++ b/homeassistant/components/scene/deconz.py @@ -38,6 +38,10 @@ async def async_added_to_hass(self): """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 8cb3915dc46e8..37fab727299a8 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -64,6 +64,11 @@ async def async_added_to_hass(self): 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 @@ def device_info(self): 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 @@ def async_update_callback(self, reason): @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 @@ def name(self): @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 @@ def should_poll(self): 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 35dbc3ef782e3..bd8167d89a083 100644 --- a/homeassistant/components/switch/deconz.py +++ b/homeassistant/components/switch/deconz.py @@ -55,6 +55,11 @@ async def async_added_to_hass(self): 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 c788a2428349c..54d902f42604f 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 8e10876cc063f..e0d1596db3f25 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 207f2f53a7fd1..7b257e223db8e 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 0000000000000..67ed9520fa4d1 --- /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 0000000000000..8e8d821108605 --- /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 0000000000000..56fc7d9fc9281 --- /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 0000000000000..158e5d619687f --- /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 0000000000000..54efe977bf917 --- /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 c1bfbbe59cd1d..e45819428e886 100644 --- a/homeassistant/components/media_player/songpal.py +++ b/homeassistant/components/media_player/songpal.py @@ -17,7 +17,7 @@ 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 54d902f42604f..327dd974edb8a 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 e273d7d6f6a54..0d6d811db7092 100644 --- a/homeassistant/components/asterisk_mbox.py +++ b/homeassistant/components/asterisk_mbox.py @@ -15,7 +15,7 @@ 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 327dd974edb8a..1b8732d53ffd8 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 | 127 ++++++++++++------------- 2 files changed, 65 insertions(+), 65 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index c756cb383d473..b86e09a6b7284 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 cf5d6fef70425..bea39354e1b43 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -167,9 +167,9 @@ def cleanup(event): 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 @@ async def service_handler(service): 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 @@ async def start_effect(self, entities, service, **kwargs): 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 @@ async def start_effect(self, entities, service, **kwargs): 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 @@ def service_to_entities(self, service): 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 - - if lifx_features(device)["multizone"]: - entity = LIFXStrip(device, self.effects_conductor) - elif lifx_features(device)["color"]: - entity = LIFXColor(device, self.effects_conductor) + bulb.timeout = MESSAGE_TIMEOUT + bulb.retry_count = MESSAGE_RETRIES + bulb.unregister_timeout = UNAVAILABLE_GRACE + + 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 @@ def __init__(self, device, effects_conductor): @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 @@ def supported_features(self): """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 @@ def supported_features(self): @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 @@ async def update_during_transition(self, when): 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 @@ async def set_state(self, **kwargs): 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 @@ async def default_effect(self, **kwargs): 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 @@ def effect_list(self): @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 @@ async def update_color_zones(self): 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 782fd8ac8ddce..7882cdc5a1510 100644 --- a/homeassistant/components/feedreader.py +++ b/homeassistant/components/feedreader.py @@ -143,7 +143,7 @@ def _update_and_fire_entry(self, entry): 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 @@ def _publish_new_entries(self): 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 dd98ebaf189c3..668f116362cab 100644 --- a/tests/components/test_feedreader.py +++ b/tests/components/test_feedreader.py @@ -160,11 +160,11 @@ def test_feed_max_length(self): 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 7b28e067cfed5..d8ccd11930648 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 2ce2ff475a2b7..9664ca9419aec 100644 --- a/homeassistant/components/wemo.py +++ b/homeassistant/components/wemo.py @@ -124,14 +124,14 @@ def setup_url_for_address(host, port): _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 da3d225bba088..5508aa76acf41 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ 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 1b8732d53ffd8..0d0acdda32fe1 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 e0d1596db3f25..bd921085461a9 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 0000000000000..1ef902e6fe206 --- /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 0000000000000..67f948e83406c --- /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 0000000000000..6caa9d49993c5 --- /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 0000000000000..e8a8037c39a7a --- /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 0000000000000..869c3b438af63 --- /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 726fa6a6cd1ed..4eb4783edd975 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 0000000000000..40a873023dd04 --- /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 0000000000000..43ec497cfb1ff --- /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 0000000000000..78999c34c2233 --- /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 0000000000000..474dbe488becb --- /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 b4b5b58f9fa7c..a716425f345fa 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 0000000000000..45b57a772f93d --- /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 0000000000000..acacddf2187f7 --- /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 6f3fa2ec9a4c0..9084d22f4a303 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 4222e7f55569a..a2ed8d21230d8 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 6e70a1f431069..f526bec4f3467 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 0000000000000..a3699db08aeb5 --- /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 0000000000000..c92d478c45420 --- /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 0c609b3430ac3..76a9adcb40efe 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 0000000000000..cf73210aa3bc0 --- /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 7ea074470c76d..c2cdb93c00511 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 4dffe492c4d84..41b097f3f8dcb 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 0000000000000..64c960a121abf --- /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 9af472893807b..e15d0dbae648a 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 0000000000000..c10cb51913348 --- /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 2566eb2557075..95e600e6d0313 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 23305a7e5844d..40d1ced5007d2 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 2266e83ac440a..87ee494a8752c 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 88c611b163361..bd065bb7506b1 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 0000000000000..62a4d7deec982 --- /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 af4cefff6e5a3..57d3a3e95e45a 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 0000000000000..14961ab98f087 --- /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 d0587036d245b..dd44fca588847 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 0aae10fde64e3..387a555219d70 100644 --- a/homeassistant/components/sensor/dht.py +++ b/homeassistant/components/sensor/dht.py @@ -17,7 +17,7 @@ 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 0d0acdda32fe1..7137c4b15d266 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 6a17345188a4d..413794505dbb2 100644 --- a/homeassistant/components/cover/myq.py +++ b/homeassistant/components/cover/myq.py @@ -120,6 +120,11 @@ def supported_features(self): """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 0000000000000..f0cf93c13e980 --- /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 212cdbac3b8bc..d79640b77ab10 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -18,7 +18,7 @@ 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 @@ def __init__(self): 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 @@ def device_state_attributes(self): 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 7137c4b15d266..ff95a8fc2addd 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 53c8e26701615..2b517652ad7e2 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -77,7 +77,7 @@ '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 4fc6b8b09540d..456252aded430 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -884,6 +884,8 @@ def source_list(self): 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 8c21e71bd3097..170d3de68002e 100644 --- a/homeassistant/components/device_tracker/google_maps.py +++ b/homeassistant/components/device_tracker/google_maps.py @@ -15,7 +15,7 @@ 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 @@ def _update_info(self, now=None): 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 5216913528a12..f709e0169cf53 100644 --- a/homeassistant/components/sensor/netatmo.py +++ b/homeassistant/components/sensor/netatmo.py @@ -317,7 +317,11 @@ def update(self): 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 2ffe58b02af2b..be6df547f1dc6 100644 --- a/homeassistant/components/media_player/yamaha.py +++ b/homeassistant/components/media_player/yamaha.py @@ -14,7 +14,7 @@ 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 @@ 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 @@ def __init__( 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 @@ def update(self): 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 @@ def source(self): """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 @@ def enable_output(self, port, enabled): """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 225f0f510adcb..64331411f7f5e 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -54,6 +54,7 @@ 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 @@ 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 @@ def name(self): """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 | 37 +++++-------------- requirements_all.txt | 2 +- 2 files changed, 10 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/sensor/enphase_envoy.py b/homeassistant/components/sensor/enphase_envoy.py index 6afe887537c83..7f8cff0f8857d 100644 --- a/homeassistant/components/sensor/enphase_envoy.py +++ b/homeassistant/components/sensor/enphase_envoy.py @@ -14,25 +14,27 @@ 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 @@ def icon(self): def update(self): """Get the energy production data from the Enphase Envoy.""" - import envoy_reader - - 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)) + from envoy_reader import EnvoyReader + + self._state = getattr(EnvoyReader(self._ip_address), self._type)() diff --git a/requirements_all.txt b/requirements_all.txt index ff95a8fc2addd..8c2080d12fac0 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 3429081910667..88a9340805600 100644 --- a/homeassistant/components/snips.py +++ b/homeassistant/components/snips.py @@ -131,7 +131,10 @@ async def message_received(topic, payload, qos): 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 baeda2c49a839..bc044999bddc1 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 5fc365342aec6..57da85fe5f6d2 100644 --- a/homeassistant/components/mqtt/server.py +++ b/homeassistant/components/mqtt/server.py @@ -13,7 +13,7 @@ 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 8c2080d12fac0..db244cc8cfbfa 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 bd921085461a9..446e3d056a39f 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 c761c47542fd8..976fdd3d15cd9 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 @@ 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 a7436cad2fcf1..e26c97887b700 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 22f8d4c37c037..29eb8cd6749e5 100644 --- a/homeassistant/components/sensor/qnap.py +++ b/homeassistant/components/sensor/qnap.py @@ -17,7 +17,7 @@ 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 @@ def __init__(self, config): 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 db244cc8cfbfa..49fc3b424669a 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 f060c9f353aeb..2ec9a2ab801d6 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -29,7 +29,7 @@ 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 @@ 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_platform(p_type, p_config, disc_info=None): 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_say_handle(service): 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 @@ async def async_get_url(self, engine, message, cache=None, language=None, 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 49fc3b424669a..64a9d6bc13385 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 fd462d6811c54..cfafcb67608d7 100644 --- a/homeassistant/components/sensor/shodan.py +++ b/homeassistant/components/sensor/shodan.py @@ -14,7 +14,7 @@ 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 64a9d6bc13385..1781a70c6caa3 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 8f2abb9be19e7..f6e982a77ef6c 100644 --- a/homeassistant/components/media_extractor.py +++ b/homeassistant/components/media_extractor.py @@ -14,7 +14,7 @@ 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 1781a70c6caa3..b2ac021d61eb3 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 897f0626b7c15..90c1f2e6795c1 100644 --- a/homeassistant/components/sensor/apcupsd.py +++ b/homeassistant/components/sensor/apcupsd.py @@ -49,6 +49,7 @@ '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 @@ '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 @@ ' 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 fec1832987892..85879b8122aad 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -251,6 +251,14 @@ async def async_set_operation_mode(self, operation_mode): # 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 ac587db13fa3e..c4e077052301e 100644 --- a/tests/components/climate/test_generic_thermostat.py +++ b/tests/components/climate/test_generic_thermostat.py @@ -21,6 +21,7 @@ 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 @@ def log_call(call): 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 2904f06b432a1..bcac038d43b53 100644 --- a/homeassistant/components/switch/tahoma.py +++ b/homeassistant/components/switch/tahoma.py @@ -12,11 +12,14 @@ 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 @@ def device_class(self): 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 @@ def toggle(self, **kwargs): @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 aaa6448916867..64071ddb03756 100644 --- a/homeassistant/components/tahoma.py +++ b/homeassistant/components/tahoma.py @@ -52,6 +52,7 @@ '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 dca47a46dbf76..0cf4bced36002 100644 --- a/homeassistant/components/notify/discord.py +++ b/homeassistant/components/notify/discord.py @@ -11,7 +11,7 @@ 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 @@ 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 @@ def async_send_message(self, message, **kwargs): 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 5369510260153..8e24716ab5764 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -22,7 +22,7 @@ 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 b2ac021d61eb3..cf761ef4aa66e 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 3e9a763181a2c..8d49d5d07c639 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 cf761ef4aa66e..41814e84b64e3 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 b1b0af70319c2..71f63adfb6451 100755 --- a/setup.py +++ b/setup.py @@ -35,7 +35,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', 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 57da85fe5f6d2..45529411ed52e 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 6ce4421ebc858..7ccd3bee4b621 100644 --- a/homeassistant/components/switch/switchmate.py +++ b/homeassistant/components/switch/switchmate.py @@ -31,11 +31,11 @@ }) -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 5508aa76acf41..72f61ddf1eb09 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ 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 41814e84b64e3..6223f9cc51cc8 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 446e3d056a39f..0ee02e0d109ab 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 0000000000000..4500098553e71 --- /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 4eb4783edd975..17fb5c56f5705 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 acacddf2187f7..d3b95121de6f3 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 55518b7da532a..27868814eab24 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 c92d478c45420..53759f9b5342f 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 0000000000000..2631843c784a1 --- /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 41b097f3f8dcb..516229c3871df 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 a310a918f6432..730f00ae62593 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 62a4d7deec982..734e82dbcd0ab 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 0000000000000..0fcd02a696147 --- /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 104412c90babf..19f9985accb41 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 57d3a3e95e45a..93b17784a4ebe 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 0000000000000..768a798e6d541 --- /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 af9a6ef54ce0a..00df456804f05 100644 --- a/homeassistant/components/media_player/onkyo.py +++ b/homeassistant/components/media_player/onkyo.py @@ -14,7 +14,7 @@ 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 @@ 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 @@ def select_source(self, source): 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 3670a5a59bdce..ebc38fcb739e6 100644 --- a/homeassistant/components/sensor/tibber.py +++ b/homeassistant/components/sensor/tibber.py @@ -151,7 +151,7 @@ def _update_current_price(self): 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 f17e7f0234480..7aec4333ea8c6 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -16,8 +16,8 @@ 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 6223f9cc51cc8..d8d73cc36a102 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 aa448ddf56ee0..de8e9783f92a1 100644 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -6,6 +6,7 @@ """ import logging import os +import socket import voluptuous as vol @@ -61,9 +62,9 @@ '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 @@ def update(self): 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 a7a4b592664d1..8634e4f457065 100644 --- a/homeassistant/components/sensor/mvglive.py +++ b/homeassistant/components/sensor/mvglive.py @@ -31,12 +31,13 @@ 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 8db09cdb8da02..6eae9e13030f3 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -325,6 +325,14 @@ async def async_remove(self, entry_id): 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 504448b948df3..e6ff45af2fe05 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -45,7 +45,7 @@ def __init__(self, hass): 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 @@ def _data_to_save(self): 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 804ee4235d088..da3645a96fe86 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -31,7 +31,7 @@ STORAGE_KEY = 'core.entity_registry' -@attr.s(slots=True, frozen=True) +@attr.s(slots=True) class RegistryEntry: """Entity Registry Entry.""" @@ -250,6 +250,14 @@ def _data_to_save(self): 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 84ad54f7b829b..a9132529bc3b0 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 d0c088a6f6971..bb28287ddd81e 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 39c31e4e40b0b..bd531e62f7253 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 0b299529a4610..c7c27d73ee42c 100644 --- a/homeassistant/components/binary_sensor/openuv.py +++ b/homeassistant/components/binary_sensor/openuv.py @@ -7,12 +7,11 @@ 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 @@ 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 @@ def _update_data(self): 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 0000000000000..df0232d01fc2f --- /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 d696f0e510027..bfd90b4a57419 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 @@ 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'] -_LOGGER = logging.getLogger(__name__) +from .config_flow import configured_instances +from .const import DOMAIN -DOMAIN = 'openuv' +REQUIREMENTS = ['pyopenuv==1.0.4'] +_LOGGER = logging.getLogger(__name__) +DATA_OPENUV_CLIENT = 'data_client' +DATA_OPENUV_LISTENER = 'data_listener' DATA_PROTECTION_WINDOW = 'protection_window' DATA_UV = 'uv' @@ -82,39 +85,77 @@ }) 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 refresh_sensors(event_time): 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 refresh_sensors(event_time): 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 0000000000000..55ee566268e6e --- /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 0000000000000..1aa3d2abcaaf6 --- /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 0000000000000..9c5af45619eef --- /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 aaa04590b3fe6..22712aa306b35 100644 --- a/homeassistant/components/sensor/openuv.py +++ b/homeassistant/components/sensor/openuv.py @@ -6,13 +6,12 @@ """ 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 @@ 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 @@ def _update_data(self): 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 @@ async def async_update(self): 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 6eae9e13030f3..15932f2c3f8ec 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -141,6 +141,7 @@ async def async_step_discovery(info): 'homematicip_cloud', 'hue', 'nest', + 'openuv', 'sonos', 'zone', ] diff --git a/requirements_all.txt b/requirements_all.txt index d8d73cc36a102..b155612350b45 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 0ee02e0d109ab..b9e4444511489 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 4b694ec7ec071..fc8e67b1ab6e1 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -78,6 +78,7 @@ '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 0000000000000..0e3595b1e5181 --- /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 0000000000000..0e50bddabdeab --- /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 | 21 +++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/scene/test_deconz.py | 1 + 6 files changed, 71 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index e9f797d95f96f..6ed0a6e2c11ac 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -24,7 +24,7 @@ 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 @@ 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_configure(call): 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 78bf7041a9329..fa0fb8e14a40c 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 5af8f657206cb..b8fca6d863074 100644 --- a/homeassistant/components/scene/deconz.py +++ b/homeassistant/components/scene/deconz.py @@ -5,8 +5,10 @@ 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 = [] - - for scene in scenes.values(): - entities.append(DeconzScene(scene)) - async_add_entities(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)) + + async_add_scene(hass.data[DATA_DECONZ].scenes.values()) class DeconzScene(Scene): diff --git a/requirements_all.txt b/requirements_all.txt index b155612350b45..5f4fd64a6a257 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 b9e4444511489..236033e2f1930 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 53f25808be21e..8c22f718fa0c4 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 71be9c2435e1b..6bb08d7e8e542 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -13,7 +13,6 @@ 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 ecbc7cb9b02b1..51bd75f66e38c 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -277,6 +277,15 @@ def test_subscribe_topic_level_wildcard_no_subtree_match(self): 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 f4eaefcae209c..91ef546ea22bd 100644 --- a/homeassistant/components/switch/dlink.py +++ b/homeassistant/components/switch/dlink.py @@ -5,14 +5,17 @@ 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_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 @@ 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 @@ def device_state_attributes(self): 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 @@ def update(self): """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 @@ def __init__(self, smartplug): 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 e50fa488b9275..41a4c2af045e0 100644 --- a/homeassistant/components/cover/rflink.py +++ b/homeassistant/components/cover/rflink.py @@ -92,9 +92,9 @@ def _handle_event(self, event): 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 @@ def should_poll(self): @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 ced1751208952..6f0fb3aba30b7 100644 --- a/homeassistant/components/sensor/miflora.py +++ b/homeassistant/components/sensor/miflora.py @@ -4,16 +4,19 @@ 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 @@ _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 @@ 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 @@ def update(self): 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 c6c0af90d15a4..43fd4cedb8864 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 turn_onoff_service_handler(service_call): 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 toggle_service_handler(service_call): 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 @@ def is_on(self) -> bool: """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 @@ def async_added_to_hass(self) -> None: # 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 @@ def async_enable(self): 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 @@ def device_state_attributes(self): } -@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 cond_func(variables): 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 if_action(variables=None): 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 7c035d7d1a5f7..e19a85edae6bd 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -45,11 +45,11 @@ def handle_event(event): # 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 74cf195bc61bd..b55d99f706a8a 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 hass_shutdown(event): # 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 b59271f25e56c..f0dcbf0be575b 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -66,7 +66,7 @@ def state_automation_listener(entity, from_s, to_s): @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 call_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 9243f960850ee..263d4158e25f7 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -43,7 +43,7 @@ def state_automation_listener(entity, from_s, to_s): @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 call_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 0fcdeaae5e09b..67a44f1a34707 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 61d846582cb5f..f30dfe753cb19 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -51,7 +51,7 @@ def zone_automation_listener(entity, from_s, to_s): # 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 zone_automation_listener(entity, from_s, to_s): '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 a45f8ba893060..247ac07283ea4 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 toggle_service(service): 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.