From 69934a9598c3fcd56153852e4d894734d5691ae1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 11 Aug 2018 08:58:52 +0200 Subject: [PATCH 01/37] Bumped version to 0.76.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 1b9dc8986a5..65505bf30ec 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 76 -PATCH_VERSION = '0.dev0' +PATCH_VERSION = '0b0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From e0229b799d59e37f9f29c2600fb37e8046b38678 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 13 Aug 2018 21:27:34 +0200 Subject: [PATCH 02/37] Update frontend to 20180813.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 e248bc20ccd..41cfdd3edd8 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180811.0'] +REQUIREMENTS = ['home-assistant-frontend==20180813.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 7f4521e3522..2f31b4359bd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -433,7 +433,7 @@ hole==0.3.0 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180811.0 +home-assistant-frontend==20180813.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cfe98bd7d4e..ffc55c23210 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==20180811.0 +home-assistant-frontend==20180813.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 985f96662e3a8cc3783e647268f8ee2ede5eb5d2 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 12 Aug 2018 20:22:54 +0200 Subject: [PATCH 03/37] Upgrade pymysensors to 0.17.0 (#15942) --- homeassistant/components/mysensors/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index 980efcf5805..e498539f2f9 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -22,7 +22,7 @@ from .const import ( from .device import get_mysensors_devices from .gateway import get_mysensors_gateway, setup_gateways, finish_setup -REQUIREMENTS = ['pymysensors==0.16.0'] +REQUIREMENTS = ['pymysensors==0.17.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 2f31b4359bd..5a2235df870 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -953,7 +953,7 @@ pymusiccast==0.1.6 pymyq==0.0.11 # homeassistant.components.mysensors -pymysensors==0.16.0 +pymysensors==0.17.0 # homeassistant.components.lock.nello pynello==1.5.1 From c0830f1c20d27f770a833f5fa4c99d1066cdf908 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 13 Aug 2018 22:39:13 +0200 Subject: [PATCH 04/37] Deprecate remote.api (#15955) --- homeassistant/remote.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 313f98a890c..c254dd500f7 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -48,6 +48,7 @@ class API: port: Optional[int] = SERVER_PORT, use_ssl: bool = False) -> None: """Init the API.""" + _LOGGER.warning('This class is deprecated and will be removed in 0.77') self.host = host self.port = port self.api_password = api_password From 9e217651730d2baac909aa6ca6112060f1ebcb64 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 13 Aug 2018 23:17:30 +0200 Subject: [PATCH 05/37] Bumped version to 0.76.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 65505bf30ec..52175c2b4e9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 76 -PATCH_VERSION = '0b0' +PATCH_VERSION = '0b1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 6d432d19fe751f533c7fb1341f9cfee7d32925ff Mon Sep 17 00:00:00 2001 From: kbickar Date: Tue, 14 Aug 2018 09:50:44 -0400 Subject: [PATCH 06/37] Added error handling for sense API timeouts (#15789) * Added error handling for sense API timeouts * Moved imports in function * Moved imports to more appropriate function * Change exception to custom package version --- homeassistant/components/sensor/sense.py | 9 +++++++-- requirements_all.txt | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/sense.py b/homeassistant/components/sensor/sense.py index 16f4ccb9b6c..89e0d15bf48 100644 --- a/homeassistant/components/sensor/sense.py +++ b/homeassistant/components/sensor/sense.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['sense_energy==0.3.1'] +REQUIREMENTS = ['sense_energy==0.4.1'] _LOGGER = logging.getLogger(__name__) @@ -139,7 +139,12 @@ class Sense(Entity): def update(self): """Get the latest data, update state.""" - self.update_sensor() + from sense_energy import SenseAPITimeoutException + try: + self.update_sensor() + except SenseAPITimeoutException: + _LOGGER.error("Timeout retrieving data") + return if self._sensor_type == ACTIVE_TYPE: if self._is_production: diff --git a/requirements_all.txt b/requirements_all.txt index 5a2235df870..0e6d7e1ac07 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1265,7 +1265,7 @@ sendgrid==5.4.1 sense-hat==2.2.0 # homeassistant.components.sensor.sense -sense_energy==0.3.1 +sense_energy==0.4.1 # homeassistant.components.media_player.aquostv sharp_aquos_rc==0.3.2 From 34e1f1b6da4d748976dbd535297d1b3ef56ec176 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Mon, 13 Aug 2018 02:27:18 -0700 Subject: [PATCH 07/37] Add context to login flow (#15914) * Add context to login flow * source -> context * Fix unit test * Update comment --- homeassistant/auth/__init__.py | 4 ++-- homeassistant/auth/providers/__init__.py | 2 +- homeassistant/auth/providers/homeassistant.py | 2 +- .../auth/providers/insecure_example.py | 2 +- .../auth/providers/legacy_api_password.py | 2 +- homeassistant/components/auth/login_flow.py | 3 +-- .../components/config/config_entries.py | 2 +- homeassistant/config_entries.py | 17 +++++------------ homeassistant/data_entry_flow.py | 8 ++++---- tests/components/cast/test_init.py | 5 +++-- tests/components/config/test_config_entries.py | 4 +--- tests/components/sonos/test_init.py | 5 +++-- tests/helpers/test_config_entry_flow.py | 3 ++- tests/test_config_entries.py | 9 ++++++--- tests/test_data_entry_flow.py | 6 ++++-- 15 files changed, 36 insertions(+), 38 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 8eaa9cdbb97..9695e77f6f1 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -215,9 +215,9 @@ class AuthManager: """Create a login flow.""" auth_provider = self._providers[handler] - return await auth_provider.async_credential_flow() + return await auth_provider.async_credential_flow(context) - async def _async_finish_login_flow(self, result): + async def _async_finish_login_flow(self, context, result): """Result of a credential login flow.""" if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: return None diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index 68cc1c7edd2..ac5b6107b8a 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -123,7 +123,7 @@ class AuthProvider: # Implement by extending class - async def async_credential_flow(self): + async def async_credential_flow(self, context): """Return the data flow for logging in with auth provider.""" raise NotImplementedError diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index e9693b09634..5a2355264ab 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -158,7 +158,7 @@ class HassAuthProvider(AuthProvider): self.data = Data(self.hass) await self.data.async_load() - async def async_credential_flow(self): + async def async_credential_flow(self, context): """Return a flow to login.""" return LoginFlow(self) diff --git a/homeassistant/auth/providers/insecure_example.py b/homeassistant/auth/providers/insecure_example.py index c86c8eb71f1..96f824140ed 100644 --- a/homeassistant/auth/providers/insecure_example.py +++ b/homeassistant/auth/providers/insecure_example.py @@ -31,7 +31,7 @@ class InvalidAuthError(HomeAssistantError): class ExampleAuthProvider(AuthProvider): """Example auth provider based on hardcoded usernames and passwords.""" - async def async_credential_flow(self): + async def async_credential_flow(self, context): """Return a flow to login.""" return LoginFlow(self) diff --git a/homeassistant/auth/providers/legacy_api_password.py b/homeassistant/auth/providers/legacy_api_password.py index 1f92fb60f13..f2f467e07ec 100644 --- a/homeassistant/auth/providers/legacy_api_password.py +++ b/homeassistant/auth/providers/legacy_api_password.py @@ -36,7 +36,7 @@ class LegacyApiPasswordAuthProvider(AuthProvider): DEFAULT_TITLE = 'Legacy API Password' - async def async_credential_flow(self): + async def async_credential_flow(self, context): """Return a flow to login.""" return LoginFlow(self) diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index 8b983b6d19f..7b80e52a8d7 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -54,7 +54,6 @@ have type "create_entry" and "result" key will contain an authorization code. "flow_id": "8f7e42faab604bcab7ac43c44ca34d58", "handler": ["insecure_example", null], "result": "411ee2f916e648d691e937ae9344681e", - "source": "user", "title": "Example", "type": "create_entry", "version": 1 @@ -152,7 +151,7 @@ class LoginFlowIndexView(HomeAssistantView): handler = data['handler'] try: - result = await self._flow_mgr.async_init(handler) + result = await self._flow_mgr.async_init(handler, context={}) except data_entry_flow.UnknownHandler: return self.json_message('Invalid handler specified', 404) except data_entry_flow.UnknownStep: diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 57fdbd31d20..04d2c713cdc 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -96,7 +96,7 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView): return self.json([ flw for flw in hass.config_entries.flow.async_progress() - if flw['source'] != config_entries.SOURCE_USER]) + if flw['context']['source'] != config_entries.SOURCE_USER]) class ConfigManagerFlowResourceView(FlowManagerResourceView): diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 51114a2a416..b2e8389e449 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -372,10 +372,10 @@ class ConfigEntries: return await entry.async_unload( self.hass, component=getattr(self.hass.components, component)) - async def _async_finish_flow(self, result): + async def _async_finish_flow(self, context, result): """Finish a config flow and add an entry.""" # If no discovery config entries in progress, remove notification. - if not any(ent['source'] in DISCOVERY_SOURCES for ent + if not any(ent['context']['source'] in DISCOVERY_SOURCES for ent in self.hass.config_entries.flow.async_progress()): self.hass.components.persistent_notification.async_dismiss( DISCOVERY_NOTIFICATION_ID) @@ -383,15 +383,12 @@ class ConfigEntries: if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: return None - source = result['source'] - if source is None: - source = SOURCE_USER entry = ConfigEntry( version=result['version'], domain=result['handler'], title=result['title'], data=result['data'], - source=source, + source=context['source'], ) self._entries.append(entry) await self._async_schedule_save() @@ -406,7 +403,7 @@ class ConfigEntries: self.hass, entry.domain, self._hass_config) # Return Entry if they not from a discovery request - if result['source'] not in DISCOVERY_SOURCES: + if context['source'] not in DISCOVERY_SOURCES: return entry return entry @@ -422,10 +419,7 @@ class ConfigEntries: if handler is None: raise data_entry_flow.UnknownHandler - if context is not None: - source = context.get('source', SOURCE_USER) - else: - source = SOURCE_USER + source = context['source'] # Make sure requirements and dependencies of component are resolved await async_process_deps_reqs( @@ -442,7 +436,6 @@ class ConfigEntries: ) flow = handler() - flow.source = source flow.init_step = source return flow diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 7609ffa615a..f820911e396 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -46,7 +46,7 @@ class FlowManager: return [{ 'flow_id': flow.flow_id, 'handler': flow.handler, - 'source': flow.source, + 'context': flow.context, } for flow in self._progress.values()] async def async_init(self, handler: Hashable, *, context: Dict = None, @@ -57,6 +57,7 @@ class FlowManager: flow.hass = self.hass flow.handler = handler flow.flow_id = uuid.uuid4().hex + flow.context = context self._progress[flow.flow_id] = flow return await self._async_handle_step(flow, flow.init_step, data) @@ -108,7 +109,7 @@ class FlowManager: self._progress.pop(flow.flow_id) # We pass a copy of the result because we're mutating our version - entry = await self._async_finish_flow(dict(result)) + entry = await self._async_finish_flow(flow.context, dict(result)) if result['type'] == RESULT_TYPE_CREATE_ENTRY: result['result'] = entry @@ -122,8 +123,8 @@ class FlowHandler: flow_id = None hass = None handler = None - source = None cur_step = None + context = None # Set by _async_create_flow callback init_step = 'init' @@ -156,7 +157,6 @@ class FlowHandler: 'handler': self.handler, 'title': title, 'data': data, - 'source': self.source, } @callback diff --git a/tests/components/cast/test_init.py b/tests/components/cast/test_init.py index 3ed9ea7b88e..1ffbd375b75 100644 --- a/tests/components/cast/test_init.py +++ b/tests/components/cast/test_init.py @@ -1,7 +1,7 @@ """Tests for the Cast config flow.""" from unittest.mock import patch -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.setup import async_setup_component from homeassistant.components import cast @@ -15,7 +15,8 @@ async def test_creating_entry_sets_up_media_player(hass): MockDependency('pychromecast', 'discovery'), \ patch('pychromecast.discovery.discover_chromecasts', return_value=True): - result = await hass.config_entries.flow.async_init(cast.DOMAIN) + result = await hass.config_entries.flow.async_init( + cast.DOMAIN, context={'source': config_entries.SOURCE_USER}) assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index f85d7df1a86..ba053050f99 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -202,7 +202,6 @@ def test_create_account(hass, client): 'handler': 'test', 'title': 'Test Entry', 'type': 'create_entry', - 'source': 'user', 'version': 1, } @@ -264,7 +263,6 @@ def test_two_step_flow(hass, client): 'type': 'create_entry', 'title': 'user-title', 'version': 1, - 'source': 'user', } @@ -295,7 +293,7 @@ def test_get_progress_index(hass, client): { 'flow_id': form['flow_id'], 'handler': 'test', - 'source': 'hassio' + 'context': {'source': 'hassio'} } ] diff --git a/tests/components/sonos/test_init.py b/tests/components/sonos/test_init.py index 9fe22fc7e79..ab4eed31fee 100644 --- a/tests/components/sonos/test_init.py +++ b/tests/components/sonos/test_init.py @@ -1,7 +1,7 @@ """Tests for the Sonos config flow.""" from unittest.mock import patch -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.setup import async_setup_component from homeassistant.components import sonos @@ -13,7 +13,8 @@ async def test_creating_entry_sets_up_media_player(hass): with patch('homeassistant.components.media_player.sonos.async_setup_entry', return_value=mock_coro(True)) as mock_setup, \ patch('soco.discover', return_value=True): - result = await hass.config_entries.flow.async_init(sonos.DOMAIN) + result = await hass.config_entries.flow.async_init( + sonos.DOMAIN, context={'source': config_entries.SOURCE_USER}) assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index 46c58320d50..9eede7dff9b 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -109,7 +109,8 @@ async def test_user_init_trumps_discovery(hass, flow_conf): assert result['type'] == data_entry_flow.RESULT_TYPE_FORM # User starts flow - result = await hass.config_entries.flow.async_init('test', data={}) + result = await hass.config_entries.flow.async_init( + 'test', context={'source': config_entries.SOURCE_USER}, data={}) assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY # Discovery flow has been aborted diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 8ac4c642b0a..1f6fd8756e6 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -116,7 +116,8 @@ def test_add_entry_calls_setup_entry(hass, manager): }) with patch.dict(config_entries.HANDLERS, {'comp': TestFlow, 'beer': 5}): - yield from manager.flow.async_init('comp') + yield from manager.flow.async_init( + 'comp', context={'source': config_entries.SOURCE_USER}) yield from hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 @@ -171,7 +172,8 @@ async def test_saving_and_loading(hass): ) with patch.dict(config_entries.HANDLERS, {'test': TestFlow}): - await hass.config_entries.flow.async_init('test') + await hass.config_entries.flow.async_init( + 'test', context={'source': config_entries.SOURCE_USER}) class Test2Flow(data_entry_flow.FlowHandler): VERSION = 3 @@ -187,7 +189,8 @@ async def test_saving_and_loading(hass): with patch('homeassistant.config_entries.HANDLERS.get', return_value=Test2Flow): - await hass.config_entries.flow.async_init('test') + await hass.config_entries.flow.async_init( + 'test', context={'source': config_entries.SOURCE_USER}) # To trigger the call_later async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index dc10f3d8d1a..c5d5bbb50bf 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -25,8 +25,10 @@ def manager(): if context is not None else 'user_input' return flow - async def async_add_entry(result): + async def async_add_entry(context, result): if (result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY): + result['source'] = context.get('source') \ + if context is not None else 'user' entries.append(result) manager = data_entry_flow.FlowManager( @@ -168,7 +170,7 @@ async def test_create_saves_data(manager): assert entry['handler'] == 'test' assert entry['title'] == 'Test Title' assert entry['data'] == 'Test Data' - assert entry['source'] == 'user_input' + assert entry['source'] == 'user' async def test_discovery_init_flow(manager): From d0e4c95bbc3a95aa4db17f504db454c86403bdc2 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Mon, 13 Aug 2018 02:26:06 -0700 Subject: [PATCH 08/37] MQTT embedded broker has to set its own password. (#15929) --- homeassistant/components/mqtt/__init__.py | 16 +++++- homeassistant/components/mqtt/server.py | 26 ++++----- tests/components/mqtt/test_server.py | 65 +++++++++++++++++++---- 3 files changed, 82 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 3928eb945aa..70d4d7aa5d7 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -32,7 +32,8 @@ from homeassistant.util.async_ import ( from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, CONF_VALUE_TEMPLATE, CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, CONF_PAYLOAD) -from homeassistant.components.mqtt.server import HBMQTT_CONFIG_SCHEMA + +from .server import HBMQTT_CONFIG_SCHEMA REQUIREMENTS = ['paho-mqtt==1.3.1'] @@ -306,7 +307,8 @@ async def _async_setup_server(hass: HomeAssistantType, return None success, broker_config = \ - await server.async_start(hass, conf.get(CONF_EMBEDDED)) + await server.async_start( + hass, conf.get(CONF_PASSWORD), conf.get(CONF_EMBEDDED)) if not success: return None @@ -349,6 +351,16 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: if CONF_EMBEDDED not in conf and CONF_BROKER in conf: broker_config = None else: + if (conf.get(CONF_PASSWORD) is None and + config.get('http') is not None and + config['http'].get('api_password') is not None): + _LOGGER.error("Starting from 0.77, embedded MQTT broker doesn't" + " use api_password as default password any more." + " Please set password configuration. See https://" + "home-assistant.io/docs/mqtt/broker#embedded-broker" + " for details") + return False + broker_config = await _async_setup_server(hass, config) if CONF_BROKER in conf: diff --git a/homeassistant/components/mqtt/server.py b/homeassistant/components/mqtt/server.py index 8a012928792..5fc365342ae 100644 --- a/homeassistant/components/mqtt/server.py +++ b/homeassistant/components/mqtt/server.py @@ -27,27 +27,29 @@ HBMQTT_CONFIG_SCHEMA = vol.Any(None, vol.Schema({ }) }, extra=vol.ALLOW_EXTRA)) +_LOGGER = logging.getLogger(__name__) + @asyncio.coroutine -def async_start(hass, server_config): +def async_start(hass, password, server_config): """Initialize MQTT Server. This method is a coroutine. """ from hbmqtt.broker import Broker, BrokerException + passwd = tempfile.NamedTemporaryFile() try: - passwd = tempfile.NamedTemporaryFile() - if server_config is None: - server_config, client_config = generate_config(hass, passwd) + server_config, client_config = generate_config( + hass, passwd, password) else: client_config = None broker = Broker(server_config, hass.loop) yield from broker.start() except BrokerException: - logging.getLogger(__name__).exception("Error initializing MQTT server") + _LOGGER.exception("Error initializing MQTT server") return False, None finally: passwd.close() @@ -63,9 +65,10 @@ def async_start(hass, server_config): return True, client_config -def generate_config(hass, passwd): +def generate_config(hass, passwd, password): """Generate a configuration based on current Home Assistant instance.""" - from homeassistant.components.mqtt import PROTOCOL_311 + from . import PROTOCOL_311 + config = { 'listeners': { 'default': { @@ -79,29 +82,26 @@ def generate_config(hass, passwd): }, }, 'auth': { - 'allow-anonymous': hass.config.api.api_password is None + 'allow-anonymous': password is None }, 'plugins': ['auth_anonymous'], } - if hass.config.api.api_password: + if password: username = 'homeassistant' - password = hass.config.api.api_password # Encrypt with what hbmqtt uses to verify from passlib.apps import custom_app_context passwd.write( 'homeassistant:{}\n'.format( - custom_app_context.encrypt( - hass.config.api.api_password)).encode('utf-8')) + custom_app_context.encrypt(password)).encode('utf-8')) passwd.flush() config['auth']['password-file'] = passwd.name config['plugins'].append('auth_file') else: username = None - password = None client_config = ('localhost', 1883, username, password, None, PROTOCOL_311) diff --git a/tests/components/mqtt/test_server.py b/tests/components/mqtt/test_server.py index 1c37c9049f3..d5d54f457d6 100644 --- a/tests/components/mqtt/test_server.py +++ b/tests/components/mqtt/test_server.py @@ -4,6 +4,7 @@ import sys import pytest +from homeassistant.const import CONF_PASSWORD from homeassistant.setup import setup_component import homeassistant.components.mqtt as mqtt @@ -19,9 +20,6 @@ class TestMQTT: def setup_method(self, method): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - setup_component(self.hass, 'http', { - 'api_password': 'super_secret' - }) def teardown_method(self, method): """Stop everything that was started.""" @@ -32,14 +30,36 @@ class TestMQTT: @patch('hbmqtt.broker.Broker', Mock(return_value=MagicMock())) @patch('hbmqtt.broker.Broker.start', Mock(return_value=mock_coro())) @patch('homeassistant.components.mqtt.MQTT') - def test_creating_config_with_http_pass(self, mock_mqtt): - """Test if the MQTT server gets started and subscribe/publish msg.""" + def test_creating_config_with_http_pass_only(self, mock_mqtt): + """Test if the MQTT server failed starts. + + Since 0.77, MQTT server has to setup its own password. + If user has api_password but don't have mqtt.password, MQTT component + will fail to start + """ mock_mqtt().async_connect.return_value = mock_coro(True) self.hass.bus.listen_once = MagicMock() - password = 'super_secret' + assert not setup_component(self.hass, mqtt.DOMAIN, { + 'http': {'api_password': 'http_secret'} + }) - self.hass.config.api = MagicMock(api_password=password) - assert setup_component(self.hass, mqtt.DOMAIN, {}) + @patch('passlib.apps.custom_app_context', Mock(return_value='')) + @patch('tempfile.NamedTemporaryFile', Mock(return_value=MagicMock())) + @patch('hbmqtt.broker.Broker', Mock(return_value=MagicMock())) + @patch('hbmqtt.broker.Broker.start', Mock(return_value=mock_coro())) + @patch('homeassistant.components.mqtt.MQTT') + def test_creating_config_with_pass_and_no_http_pass(self, mock_mqtt): + """Test if the MQTT server gets started with password. + + Since 0.77, MQTT server has to setup its own password. + """ + mock_mqtt().async_connect.return_value = mock_coro(True) + self.hass.bus.listen_once = MagicMock() + password = 'mqtt_secret' + + assert setup_component(self.hass, mqtt.DOMAIN, { + mqtt.DOMAIN: {CONF_PASSWORD: password}, + }) assert mock_mqtt.called from pprint import pprint pprint(mock_mqtt.mock_calls) @@ -51,8 +71,33 @@ class TestMQTT: @patch('hbmqtt.broker.Broker', Mock(return_value=MagicMock())) @patch('hbmqtt.broker.Broker.start', Mock(return_value=mock_coro())) @patch('homeassistant.components.mqtt.MQTT') - def test_creating_config_with_http_no_pass(self, mock_mqtt): - """Test if the MQTT server gets started and subscribe/publish msg.""" + def test_creating_config_with_pass_and_http_pass(self, mock_mqtt): + """Test if the MQTT server gets started with password. + + Since 0.77, MQTT server has to setup its own password. + """ + mock_mqtt().async_connect.return_value = mock_coro(True) + self.hass.bus.listen_once = MagicMock() + password = 'mqtt_secret' + + self.hass.config.api = MagicMock(api_password='api_password') + assert setup_component(self.hass, mqtt.DOMAIN, { + 'http': {'api_password': 'http_secret'}, + mqtt.DOMAIN: {CONF_PASSWORD: password}, + }) + assert mock_mqtt.called + from pprint import pprint + pprint(mock_mqtt.mock_calls) + assert mock_mqtt.mock_calls[1][1][5] == 'homeassistant' + assert mock_mqtt.mock_calls[1][1][6] == password + + @patch('passlib.apps.custom_app_context', Mock(return_value='')) + @patch('tempfile.NamedTemporaryFile', Mock(return_value=MagicMock())) + @patch('hbmqtt.broker.Broker', Mock(return_value=MagicMock())) + @patch('hbmqtt.broker.Broker.start', Mock(return_value=mock_coro())) + @patch('homeassistant.components.mqtt.MQTT') + def test_creating_config_without_pass(self, mock_mqtt): + """Test if the MQTT server gets started without password.""" mock_mqtt().async_connect.return_value = mock_coro(True) self.hass.bus.listen_once = MagicMock() From d393380122d0092a7b2b127e7722dbd8e27972af Mon Sep 17 00:00:00 2001 From: Khalid Date: Tue, 14 Aug 2018 12:55:40 +0300 Subject: [PATCH 09/37] Fix issue when reading worxlandroid pin code (#15930) Fixes #14050 --- homeassistant/components/sensor/worxlandroid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/worxlandroid.py b/homeassistant/components/sensor/worxlandroid.py index c49ce36bd49..8963bb135e0 100644 --- a/homeassistant/components/sensor/worxlandroid.py +++ b/homeassistant/components/sensor/worxlandroid.py @@ -28,7 +28,7 @@ DEFAULT_TIMEOUT = 5 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PIN): - vol.All(vol.Coerce(int), vol.Range(min=1000, max=9999)), + vol.All(vol.Coerce(str), vol.Match(r'\d{4}')), vol.Optional(CONF_ALLOW_UNREACHABLE, default=True): cv.boolean, vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, }) From f4e84fbf84f1526b5e4a0a47a16a51797eb9961d Mon Sep 17 00:00:00 2001 From: Daniel Bowman Date: Tue, 14 Aug 2018 10:53:08 +0100 Subject: [PATCH 10/37] remove-phantomjs-from-docker (#15936) --- Dockerfile | 1 - virtualization/Docker/Dockerfile.dev | 1 - virtualization/Docker/scripts/phantomjs | 15 --------------- virtualization/Docker/setup_docker_prereqs | 5 ----- 4 files changed, 22 deletions(-) delete mode 100755 virtualization/Docker/scripts/phantomjs diff --git a/Dockerfile b/Dockerfile index 75d9e9eb716..c84e6162d04 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,6 @@ LABEL maintainer="Paulus Schoutsen " #ENV INSTALL_OPENALPR no #ENV INSTALL_FFMPEG no #ENV INSTALL_LIBCEC no -#ENV INSTALL_PHANTOMJS no #ENV INSTALL_SSOCR no #ENV INSTALL_IPERF3 no diff --git a/virtualization/Docker/Dockerfile.dev b/virtualization/Docker/Dockerfile.dev index d0599c2e74c..79072703031 100644 --- a/virtualization/Docker/Dockerfile.dev +++ b/virtualization/Docker/Dockerfile.dev @@ -10,7 +10,6 @@ LABEL maintainer="Paulus Schoutsen " #ENV INSTALL_OPENALPR no #ENV INSTALL_FFMPEG no #ENV INSTALL_LIBCEC no -#ENV INSTALL_PHANTOMJS no #ENV INSTALL_COAP no #ENV INSTALL_SSOCR no #ENV INSTALL_IPERF3 no diff --git a/virtualization/Docker/scripts/phantomjs b/virtualization/Docker/scripts/phantomjs deleted file mode 100755 index 7700b08f293..00000000000 --- a/virtualization/Docker/scripts/phantomjs +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -# Sets up phantomjs. - -# Stop on errors -set -e - -PHANTOMJS_VERSION="2.1.1" - -cd /usr/src/app/ -mkdir -p build && cd build - -curl -LSO https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2 -tar -xjf phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2 -mv phantomjs-$PHANTOMJS_VERSION-linux-x86_64/bin/phantomjs /usr/bin/phantomjs -/usr/bin/phantomjs -v \ No newline at end of file diff --git a/virtualization/Docker/setup_docker_prereqs b/virtualization/Docker/setup_docker_prereqs index 15504ea57af..65acf92b855 100755 --- a/virtualization/Docker/setup_docker_prereqs +++ b/virtualization/Docker/setup_docker_prereqs @@ -7,7 +7,6 @@ set -e INSTALL_TELLSTICK="${INSTALL_TELLSTICK:-yes}" INSTALL_OPENALPR="${INSTALL_OPENALPR:-yes}" INSTALL_LIBCEC="${INSTALL_LIBCEC:-yes}" -INSTALL_PHANTOMJS="${INSTALL_PHANTOMJS:-yes}" INSTALL_SSOCR="${INSTALL_SSOCR:-yes}" # Required debian packages for running hass or components @@ -59,10 +58,6 @@ if [ "$INSTALL_LIBCEC" == "yes" ]; then virtualization/Docker/scripts/libcec fi -if [ "$INSTALL_PHANTOMJS" == "yes" ]; then - virtualization/Docker/scripts/phantomjs -fi - if [ "$INSTALL_SSOCR" == "yes" ]; then virtualization/Docker/scripts/ssocr fi From 1b384c322a4fd60404bf344cdd352e571e213e20 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Mon, 13 Aug 2018 00:26:20 -0700 Subject: [PATCH 11/37] Remove remote.API from core.Config (#15951) * Use core.ApiConfig replace remote.API in core.Config * Move ApiConfig to http --- homeassistant/components/http/__init__.py | 28 ++++++++++++-- homeassistant/core.py | 4 +- tests/components/http/test_init.py | 45 +++++++++++++++++++++++ 3 files changed, 72 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 9f1b5995839..c1d80667983 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -8,6 +8,7 @@ from ipaddress import ip_network import logging import os import ssl +from typing import Optional from aiohttp import web from aiohttp.web_exceptions import HTTPMovedPermanently @@ -16,7 +17,6 @@ import voluptuous as vol from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, SERVER_PORT) import homeassistant.helpers.config_validation as cv -import homeassistant.remote as rem import homeassistant.util as hass_util from homeassistant.util.logging import HideSensitiveDataFilter from homeassistant.util import ssl as ssl_util @@ -82,6 +82,28 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) +class ApiConfig: + """Configuration settings for API server.""" + + def __init__(self, host: str, port: Optional[int] = SERVER_PORT, + use_ssl: bool = False, + api_password: Optional[str] = None) -> None: + """Initialize a new API config object.""" + self.host = host + self.port = port + self.api_password = api_password + + if host.startswith(("http://", "https://")): + self.base_url = host + elif use_ssl: + self.base_url = "https://{}".format(host) + else: + self.base_url = "http://{}".format(host) + + if port is not None: + self.base_url += ':{}'.format(port) + + async def async_setup(hass, config): """Set up the HTTP API and debug interface.""" conf = config.get(DOMAIN) @@ -146,8 +168,8 @@ async def async_setup(hass, config): host = hass_util.get_local_ip() port = server_port - hass.config.api = rem.API(host, api_password, port, - ssl_certificate is not None) + hass.config.api = ApiConfig(host, port, ssl_certificate is not None, + api_password) return True diff --git a/homeassistant/core.py b/homeassistant/core.py index cc027c6f5d0..2b7a2479471 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1145,8 +1145,8 @@ class Config: # List of loaded components self.components = set() # type: set - # Remote.API object pointing at local API - self.api = None + # API (HTTP) server configuration + self.api = None # type: Optional[Any] # Directory that holds the configuration self.config_dir = None # type: Optional[str] diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 2ffaf17bebc..c52f60a5f1b 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -1,5 +1,6 @@ """The tests for the Home Assistant HTTP component.""" import logging +import unittest from homeassistant.setup import async_setup_component @@ -33,6 +34,50 @@ async def test_registering_view_while_running(hass, aiohttp_client, hass.http.register_view(TestView) +class TestApiConfig(unittest.TestCase): + """Test API configuration methods.""" + + def test_api_base_url_with_domain(hass): + """Test setting API URL with domain.""" + api_config = http.ApiConfig('example.com') + assert api_config.base_url == 'http://example.com:8123' + + def test_api_base_url_with_ip(hass): + """Test setting API URL with IP.""" + api_config = http.ApiConfig('1.1.1.1') + assert api_config.base_url == 'http://1.1.1.1:8123' + + def test_api_base_url_with_ip_and_port(hass): + """Test setting API URL with IP and port.""" + api_config = http.ApiConfig('1.1.1.1', 8124) + assert api_config.base_url == 'http://1.1.1.1:8124' + + def test_api_base_url_with_protocol(hass): + """Test setting API URL with protocol.""" + api_config = http.ApiConfig('https://example.com') + assert api_config.base_url == 'https://example.com:8123' + + def test_api_base_url_with_protocol_and_port(hass): + """Test setting API URL with protocol and port.""" + api_config = http.ApiConfig('https://example.com', 433) + assert api_config.base_url == 'https://example.com:433' + + def test_api_base_url_with_ssl_enable(hass): + """Test setting API URL with use_ssl enabled.""" + api_config = http.ApiConfig('example.com', use_ssl=True) + assert api_config.base_url == 'https://example.com:8123' + + def test_api_base_url_with_ssl_enable_and_port(hass): + """Test setting API URL with use_ssl enabled and port.""" + api_config = http.ApiConfig('1.1.1.1', use_ssl=True, port=8888) + assert api_config.base_url == 'https://1.1.1.1:8888' + + def test_api_base_url_with_protocol_and_ssl_enable(hass): + """Test setting API URL with specific protocol and use_ssl enabled.""" + api_config = http.ApiConfig('http://example.com', use_ssl=True) + assert api_config.base_url == 'http://example.com:8123' + + async def test_api_base_url_with_domain(hass): """Test setting API URL.""" result = await async_setup_component(hass, 'http', { From 899c2057b739977974ad72995d4d7e385a25c307 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 14 Aug 2018 08:20:17 +0200 Subject: [PATCH 12/37] Switch to intermediate Mozilla cert profile (#15957) * Allow choosing intermediate SSL profile * Fix tests --- homeassistant/components/http/__init__.py | 20 ++++++-- homeassistant/util/ssl.py | 56 ++++++++++++++++++++++- tests/components/http/test_init.py | 56 +++++++++++++++++++++++ tests/scripts/test_check_config.py | 4 +- 4 files changed, 130 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index c1d80667983..9ba977f92f5 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -49,6 +49,10 @@ CONF_TRUSTED_PROXIES = 'trusted_proxies' CONF_TRUSTED_NETWORKS = 'trusted_networks' CONF_LOGIN_ATTEMPTS_THRESHOLD = 'login_attempts_threshold' CONF_IP_BAN_ENABLED = 'ip_ban_enabled' +CONF_SSL_PROFILE = 'ssl_profile' + +SSL_MODERN = 'modern' +SSL_INTERMEDIATE = 'intermediate' _LOGGER = logging.getLogger(__name__) @@ -74,7 +78,9 @@ HTTP_SCHEMA = vol.Schema({ vol.Optional(CONF_LOGIN_ATTEMPTS_THRESHOLD, default=NO_LOGIN_ATTEMPT_THRESHOLD): vol.Any(cv.positive_int, NO_LOGIN_ATTEMPT_THRESHOLD), - vol.Optional(CONF_IP_BAN_ENABLED, default=True): cv.boolean + vol.Optional(CONF_IP_BAN_ENABLED, default=True): cv.boolean, + vol.Optional(CONF_SSL_PROFILE, default=SSL_MODERN): + vol.In([SSL_INTERMEDIATE, SSL_MODERN]), }) CONFIG_SCHEMA = vol.Schema({ @@ -123,6 +129,7 @@ async def async_setup(hass, config): trusted_networks = conf[CONF_TRUSTED_NETWORKS] is_ban_enabled = conf[CONF_IP_BAN_ENABLED] login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD] + ssl_profile = conf[CONF_SSL_PROFILE] if api_password is not None: logging.getLogger('aiohttp.access').addFilter( @@ -141,7 +148,8 @@ async def async_setup(hass, config): trusted_proxies=trusted_proxies, trusted_networks=trusted_networks, login_threshold=login_threshold, - is_ban_enabled=is_ban_enabled + is_ban_enabled=is_ban_enabled, + ssl_profile=ssl_profile, ) async def stop_server(event): @@ -181,7 +189,7 @@ class HomeAssistantHTTP: ssl_certificate, ssl_peer_certificate, ssl_key, server_host, server_port, cors_origins, use_x_forwarded_for, trusted_proxies, trusted_networks, - login_threshold, is_ban_enabled): + login_threshold, is_ban_enabled, ssl_profile): """Initialize the HTTP Home Assistant server.""" app = self.app = web.Application( middlewares=[staticresource_middleware]) @@ -221,6 +229,7 @@ class HomeAssistantHTTP: self.server_host = server_host self.server_port = server_port self.is_ban_enabled = is_ban_enabled + self.ssl_profile = ssl_profile self._handler = None self.server = None @@ -307,7 +316,10 @@ class HomeAssistantHTTP: if self.ssl_certificate: try: - context = ssl_util.server_context() + if self.ssl_profile == SSL_INTERMEDIATE: + context = ssl_util.server_context_intermediate() + else: + context = ssl_util.server_context_modern() context.load_cert_chain(self.ssl_certificate, self.ssl_key) except OSError as error: _LOGGER.error("Could not read SSL certificate from %s: %s", diff --git a/homeassistant/util/ssl.py b/homeassistant/util/ssl.py index 392c5986c89..b78395cdb0d 100644 --- a/homeassistant/util/ssl.py +++ b/homeassistant/util/ssl.py @@ -13,7 +13,7 @@ def client_context() -> ssl.SSLContext: return context -def server_context() -> ssl.SSLContext: +def server_context_modern() -> ssl.SSLContext: """Return an SSL context following the Mozilla recommendations. TLS configuration follows the best-practice guidelines specified here: @@ -37,4 +37,58 @@ def server_context() -> ssl.SSLContext: "ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:" "ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256" ) + + return context + + +def server_context_intermediate() -> ssl.SSLContext: + """Return an SSL context following the Mozilla recommendations. + + TLS configuration follows the best-practice guidelines specified here: + https://wiki.mozilla.org/Security/Server_Side_TLS + Intermediate guidelines are followed. + """ + context = ssl.SSLContext(ssl.PROTOCOL_TLS) # pylint: disable=no-member + + context.options |= ( + ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | + ssl.OP_CIPHER_SERVER_PREFERENCE + ) + if hasattr(ssl, 'OP_NO_COMPRESSION'): + context.options |= ssl.OP_NO_COMPRESSION + + context.set_ciphers( + "ECDHE-ECDSA-CHACHA20-POLY1305:" + "ECDHE-RSA-CHACHA20-POLY1305:" + "ECDHE-ECDSA-AES128-GCM-SHA256:" + "ECDHE-RSA-AES128-GCM-SHA256:" + "ECDHE-ECDSA-AES256-GCM-SHA384:" + "ECDHE-RSA-AES256-GCM-SHA384:" + "DHE-RSA-AES128-GCM-SHA256:" + "DHE-RSA-AES256-GCM-SHA384:" + "ECDHE-ECDSA-AES128-SHA256:" + "ECDHE-RSA-AES128-SHA256:" + "ECDHE-ECDSA-AES128-SHA:" + "ECDHE-RSA-AES256-SHA384:" + "ECDHE-RSA-AES128-SHA:" + "ECDHE-ECDSA-AES256-SHA384:" + "ECDHE-ECDSA-AES256-SHA:" + "ECDHE-RSA-AES256-SHA:" + "DHE-RSA-AES128-SHA256:" + "DHE-RSA-AES128-SHA:" + "DHE-RSA-AES256-SHA256:" + "DHE-RSA-AES256-SHA:" + "ECDHE-ECDSA-DES-CBC3-SHA:" + "ECDHE-RSA-DES-CBC3-SHA:" + "EDH-RSA-DES-CBC3-SHA:" + "AES128-GCM-SHA256:" + "AES256-GCM-SHA384:" + "AES128-SHA256:" + "AES256-SHA256:" + "AES128-SHA:" + "AES256-SHA:" + "DES-CBC3-SHA:" + "!DSS" + ) + return context diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index c52f60a5f1b..9f6441c5238 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -1,10 +1,13 @@ """The tests for the Home Assistant HTTP component.""" import logging import unittest +from unittest.mock import patch from homeassistant.setup import async_setup_component import homeassistant.components.http as http +from homeassistant.util.ssl import ( + server_context_modern, server_context_intermediate) class TestView(http.HomeAssistantView): @@ -169,3 +172,56 @@ async def test_proxy_config_only_trust_proxies(hass): http.CONF_TRUSTED_PROXIES: ['127.0.0.1'] } }) is not True + + +async def test_ssl_profile_defaults_modern(hass): + """Test default ssl profile.""" + assert await async_setup_component(hass, 'http', {}) is True + + hass.http.ssl_certificate = 'bla' + + with patch('ssl.SSLContext.load_cert_chain'), \ + patch('homeassistant.util.ssl.server_context_modern', + side_effect=server_context_modern) as mock_context: + await hass.async_start() + await hass.async_block_till_done() + + assert len(mock_context.mock_calls) == 1 + + +async def test_ssl_profile_change_intermediate(hass): + """Test setting ssl profile to intermediate.""" + assert await async_setup_component(hass, 'http', { + 'http': { + 'ssl_profile': 'intermediate' + } + }) is True + + hass.http.ssl_certificate = 'bla' + + with patch('ssl.SSLContext.load_cert_chain'), \ + patch('homeassistant.util.ssl.server_context_intermediate', + side_effect=server_context_intermediate) as mock_context: + await hass.async_start() + await hass.async_block_till_done() + + assert len(mock_context.mock_calls) == 1 + + +async def test_ssl_profile_change_modern(hass): + """Test setting ssl profile to modern.""" + assert await async_setup_component(hass, 'http', { + 'http': { + 'ssl_profile': 'modern' + } + }) is True + + hass.http.ssl_certificate = 'bla' + + with patch('ssl.SSLContext.load_cert_chain'), \ + patch('homeassistant.util.ssl.server_context_modern', + side_effect=server_context_modern) as mock_context: + await hass.async_start() + await hass.async_block_till_done() + + assert len(mock_context.mock_calls) == 1 diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 59d8e27a672..532197b4072 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -159,7 +159,9 @@ class TestCheckConfig(unittest.TestCase): 'login_attempts_threshold': -1, 'server_host': '0.0.0.0', 'server_port': 8123, - 'trusted_networks': []} + 'trusted_networks': [], + 'ssl_profile': 'modern', + } assert res['secret_cache'] == {secrets_path: {'http_pw': 'abc123'}} assert res['secrets'] == {'http_pw': 'abc123'} assert normalize_yaml_files(res) == [ From f5df567d099f6bdef1e0b53b38d36e48e9f58c0e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 14 Aug 2018 21:14:12 +0200 Subject: [PATCH 13/37] Use JWT for access tokens (#15972) * Use JWT for access tokens * Update requirements * Improvements --- homeassistant/auth/__init__.py | 64 +++++++++++++------ homeassistant/auth/auth_store.py | 56 ++++++++-------- homeassistant/auth/models.py | 22 +------ homeassistant/components/auth/__init__.py | 6 +- homeassistant/components/http/auth.py | 6 +- homeassistant/components/websocket_api.py | 9 +-- homeassistant/package_constraints.txt | 1 + requirements_all.txt | 1 + setup.py | 1 + tests/auth/test_init.py | 45 ++++--------- tests/common.py | 14 ++-- tests/components/auth/test_init.py | 24 +++++-- tests/components/auth/test_init_link_user.py | 2 +- tests/components/config/test_auth.py | 16 +++-- .../test_auth_provider_homeassistant.py | 38 ++++++++--- tests/components/conftest.py | 2 +- tests/components/hassio/test_init.py | 6 +- tests/components/http/test_auth.py | 8 ++- tests/components/test_api.py | 22 +++++-- tests/components/test_websocket_api.py | 15 +++-- 20 files changed, 203 insertions(+), 155 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 9695e77f6f1..148f97702e3 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -4,10 +4,12 @@ import logging from collections import OrderedDict from typing import List, Awaitable +import jwt + from homeassistant import data_entry_flow from homeassistant.core import callback, HomeAssistant +from homeassistant.util import dt as dt_util -from . import models from . import auth_store from .providers import auth_provider_from_config @@ -54,7 +56,6 @@ class AuthManager: self.login_flow = data_entry_flow.FlowManager( hass, self._async_create_login_flow, self._async_finish_login_flow) - self._access_tokens = OrderedDict() @property def active(self): @@ -181,35 +182,56 @@ class AuthManager: return await self._store.async_create_refresh_token(user, client_id) - async def async_get_refresh_token(self, token): + async def async_get_refresh_token(self, token_id): + """Get refresh token by id.""" + return await self._store.async_get_refresh_token(token_id) + + async def async_get_refresh_token_by_token(self, token): """Get refresh token by token.""" - return await self._store.async_get_refresh_token(token) + return await self._store.async_get_refresh_token_by_token(token) @callback def async_create_access_token(self, refresh_token): """Create a new access token.""" - access_token = models.AccessToken(refresh_token=refresh_token) - self._access_tokens[access_token.token] = access_token - return access_token + # pylint: disable=no-self-use + return jwt.encode({ + 'iss': refresh_token.id, + 'iat': dt_util.utcnow(), + 'exp': dt_util.utcnow() + refresh_token.access_token_expiration, + }, refresh_token.jwt_key, algorithm='HS256').decode() - @callback - def async_get_access_token(self, token): - """Get an access token.""" - tkn = self._access_tokens.get(token) - - if tkn is None: - _LOGGER.debug('Attempt to get non-existing access token') + async def async_validate_access_token(self, token): + """Return if an access token is valid.""" + try: + unverif_claims = jwt.decode(token, verify=False) + except jwt.InvalidTokenError: return None - if tkn.expired or not tkn.refresh_token.user.is_active: - if tkn.expired: - _LOGGER.debug('Attempt to get expired access token') - else: - _LOGGER.debug('Attempt to get access token for inactive user') - self._access_tokens.pop(token) + refresh_token = await self.async_get_refresh_token( + unverif_claims.get('iss')) + + if refresh_token is None: + jwt_key = '' + issuer = '' + else: + jwt_key = refresh_token.jwt_key + issuer = refresh_token.id + + try: + jwt.decode( + token, + jwt_key, + leeway=10, + issuer=issuer, + algorithms=['HS256'] + ) + except jwt.InvalidTokenError: return None - return tkn + if not refresh_token.user.is_active: + return None + + return refresh_token async def _async_create_login_flow(self, handler, *, context, data): """Create a login flow.""" diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index 8fd66d4bbb7..806cd109d78 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -1,6 +1,7 @@ """Storage for auth models.""" from collections import OrderedDict from datetime import timedelta +import hmac from homeassistant.util import dt as dt_util @@ -110,22 +111,36 @@ class AuthStore: async def async_create_refresh_token(self, user, client_id=None): """Create a new token for a user.""" refresh_token = models.RefreshToken(user=user, client_id=client_id) - user.refresh_tokens[refresh_token.token] = refresh_token + user.refresh_tokens[refresh_token.id] = refresh_token await self.async_save() return refresh_token - async def async_get_refresh_token(self, token): - """Get refresh token by token.""" + async def async_get_refresh_token(self, token_id): + """Get refresh token by id.""" if self._users is None: await self.async_load() for user in self._users.values(): - refresh_token = user.refresh_tokens.get(token) + refresh_token = user.refresh_tokens.get(token_id) if refresh_token is not None: return refresh_token return None + async def async_get_refresh_token_by_token(self, token): + """Get refresh token by token.""" + if self._users is None: + await self.async_load() + + found = None + + for user in self._users.values(): + for refresh_token in user.refresh_tokens.values(): + if hmac.compare_digest(refresh_token.token, token): + found = refresh_token + + return found + async def async_load(self): """Load the users.""" data = await self._store.async_load() @@ -153,9 +168,11 @@ class AuthStore: data=cred_dict['data'], )) - refresh_tokens = OrderedDict() - for rt_dict in data['refresh_tokens']: + # Filter out the old keys that don't have jwt_key (pre-0.76) + if 'jwt_key' not in rt_dict: + continue + token = models.RefreshToken( id=rt_dict['id'], user=users[rt_dict['user_id']], @@ -164,18 +181,9 @@ class AuthStore: access_token_expiration=timedelta( seconds=rt_dict['access_token_expiration']), token=rt_dict['token'], + jwt_key=rt_dict['jwt_key'] ) - refresh_tokens[token.id] = token - users[rt_dict['user_id']].refresh_tokens[token.token] = token - - for ac_dict in data['access_tokens']: - refresh_token = refresh_tokens[ac_dict['refresh_token_id']] - token = models.AccessToken( - refresh_token=refresh_token, - created_at=dt_util.parse_datetime(ac_dict['created_at']), - token=ac_dict['token'], - ) - refresh_token.access_tokens.append(token) + users[rt_dict['user_id']].refresh_tokens[token.id] = token self._users = users @@ -213,27 +221,15 @@ class AuthStore: 'access_token_expiration': refresh_token.access_token_expiration.total_seconds(), 'token': refresh_token.token, + 'jwt_key': refresh_token.jwt_key, } for user in self._users.values() for refresh_token in user.refresh_tokens.values() ] - access_tokens = [ - { - 'id': user.id, - 'refresh_token_id': refresh_token.id, - 'created_at': access_token.created_at.isoformat(), - 'token': access_token.token, - } - for user in self._users.values() - for refresh_token in user.refresh_tokens.values() - for access_token in refresh_token.access_tokens - ] - data = { 'users': users, 'credentials': credentials, - 'access_tokens': access_tokens, 'refresh_tokens': refresh_tokens, } diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index 38e054dc7cf..3f49c56bce6 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -39,26 +39,8 @@ class RefreshToken: default=ACCESS_TOKEN_EXPIRATION) token = attr.ib(type=str, default=attr.Factory(lambda: generate_secret(64))) - access_tokens = attr.ib(type=list, default=attr.Factory(list), cmp=False) - - -@attr.s(slots=True) -class AccessToken: - """Access token to access the API. - - These will only ever be stored in memory and not be persisted. - """ - - refresh_token = attr.ib(type=RefreshToken) - created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow)) - token = attr.ib(type=str, - default=attr.Factory(generate_secret)) - - @property - def expired(self): - """Return if this token has expired.""" - expires = self.created_at + self.refresh_token.access_token_expiration - return dt_util.utcnow() > expires + jwt_key = attr.ib(type=str, + default=attr.Factory(lambda: generate_secret(64))) @attr.s(slots=True) diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index 0b2b4fb1a2e..102bfe58b55 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -155,7 +155,7 @@ class GrantTokenView(HomeAssistantView): access_token = hass.auth.async_create_access_token(refresh_token) return self.json({ - 'access_token': access_token.token, + 'access_token': access_token, 'token_type': 'Bearer', 'refresh_token': refresh_token.token, 'expires_in': @@ -178,7 +178,7 @@ class GrantTokenView(HomeAssistantView): 'error': 'invalid_request', }, status_code=400) - refresh_token = await hass.auth.async_get_refresh_token(token) + refresh_token = await hass.auth.async_get_refresh_token_by_token(token) if refresh_token is None: return self.json({ @@ -193,7 +193,7 @@ class GrantTokenView(HomeAssistantView): access_token = hass.auth.async_create_access_token(refresh_token) return self.json({ - 'access_token': access_token.token, + 'access_token': access_token, 'token_type': 'Bearer', 'expires_in': int(refresh_token.access_token_expiration.total_seconds()), diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 77621e3bc7c..d01d1b50c5a 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -106,11 +106,11 @@ async def async_validate_auth_header(request, api_password=None): if auth_type == 'Bearer': hass = request.app['hass'] - access_token = hass.auth.async_get_access_token(auth_val) - if access_token is None: + refresh_token = await hass.auth.async_validate_access_token(auth_val) + if refresh_token is None: return False - request['hass_user'] = access_token.refresh_token.user + request['hass_user'] = refresh_token.user return True if auth_type == 'Basic' and api_password is not None: diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index d9c92fa357f..532f3672df4 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -352,11 +352,12 @@ class ActiveConnection: if self.hass.auth.active and 'access_token' in msg: self.debug("Received access_token") - token = self.hass.auth.async_get_access_token( - msg['access_token']) - authenticated = token is not None + refresh_token = \ + await self.hass.auth.async_validate_access_token( + msg['access_token']) + authenticated = refresh_token is not None if authenticated: - request['hass_user'] = token.refresh_token.user + request['hass_user'] = refresh_token.user elif ((not self.hass.auth.active or self.hass.auth.support_legacy) and diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 29e10838f21..3aa1e3643c6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,6 +4,7 @@ async_timeout==3.0.0 attrs==18.1.0 certifi>=2018.04.16 jinja2>=2.10 +PyJWT==1.6.4 pip>=8.0.3 pytz>=2018.04 pyyaml>=3.13,<4 diff --git a/requirements_all.txt b/requirements_all.txt index 0e6d7e1ac07..3f50e50d19a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,6 +5,7 @@ async_timeout==3.0.0 attrs==18.1.0 certifi>=2018.04.16 jinja2>=2.10 +PyJWT==1.6.4 pip>=8.0.3 pytz>=2018.04 pyyaml>=3.13,<4 diff --git a/setup.py b/setup.py index b319df9067d..bd1e70aa8ae 100755 --- a/setup.py +++ b/setup.py @@ -38,6 +38,7 @@ REQUIRES = [ 'attrs==18.1.0', 'certifi>=2018.04.16', 'jinja2>=2.10', + 'PyJWT==1.6.4', 'pip>=8.0.3', 'pytz>=2018.04', 'pyyaml>=3.13,<4', diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index cad4bbdbd71..da5daca7cf6 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -199,9 +199,7 @@ async def test_saving_loading(hass, hass_storage): }) user = await manager.async_get_or_create_user(step['result']) await manager.async_activate_user(user) - refresh_token = await manager.async_create_refresh_token(user, CLIENT_ID) - - manager.async_create_access_token(refresh_token) + await manager.async_create_refresh_token(user, CLIENT_ID) await flush_store(manager._store._store) @@ -211,30 +209,6 @@ async def test_saving_loading(hass, hass_storage): assert users[0] == user -def test_access_token_expired(): - """Test that the expired property on access tokens work.""" - refresh_token = auth_models.RefreshToken( - user=None, - client_id='bla' - ) - - access_token = auth_models.AccessToken( - refresh_token=refresh_token - ) - - assert access_token.expired is False - - with patch('homeassistant.util.dt.utcnow', - return_value=dt_util.utcnow() + - auth_const.ACCESS_TOKEN_EXPIRATION): - assert access_token.expired is True - - almost_exp = \ - dt_util.utcnow() + auth_const.ACCESS_TOKEN_EXPIRATION - timedelta(1) - with patch('homeassistant.util.dt.utcnow', return_value=almost_exp): - assert access_token.expired is False - - async def test_cannot_retrieve_expired_access_token(hass): """Test that we cannot retrieve expired access tokens.""" manager = await auth.auth_manager_from_config(hass, []) @@ -244,15 +218,20 @@ async def test_cannot_retrieve_expired_access_token(hass): assert refresh_token.client_id == CLIENT_ID access_token = manager.async_create_access_token(refresh_token) - assert manager.async_get_access_token(access_token.token) is access_token + assert ( + await manager.async_validate_access_token(access_token) + is refresh_token + ) with patch('homeassistant.util.dt.utcnow', - return_value=dt_util.utcnow() + - auth_const.ACCESS_TOKEN_EXPIRATION): - assert manager.async_get_access_token(access_token.token) is None + return_value=dt_util.utcnow() - + auth_const.ACCESS_TOKEN_EXPIRATION - timedelta(seconds=11)): + access_token = manager.async_create_access_token(refresh_token) - # Even with unpatched time, it should have been removed from manager - assert manager.async_get_access_token(access_token.token) is None + assert ( + await manager.async_validate_access_token(access_token) + is None + ) async def test_generating_system_user(hass): diff --git a/tests/common.py b/tests/common.py index df333cca735..81e4774ccd4 100644 --- a/tests/common.py +++ b/tests/common.py @@ -314,12 +314,18 @@ def mock_registry(hass, mock_entries=None): class MockUser(auth_models.User): """Mock a user in Home Assistant.""" - def __init__(self, id='mock-id', is_owner=False, is_active=True, + def __init__(self, id=None, is_owner=False, is_active=True, name='Mock User', system_generated=False): """Initialize mock user.""" - super().__init__( - id=id, is_owner=is_owner, is_active=is_active, name=name, - system_generated=system_generated) + kwargs = { + 'is_owner': is_owner, + 'is_active': is_active, + 'name': name, + 'system_generated': system_generated + } + if id is not None: + kwargs['id'] = id + super().__init__(**kwargs) def add_to_hass(self, hass): """Test helper to add entry to hass.""" diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index eea768c96a0..f1a1bb5bd3c 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -44,7 +44,10 @@ async def test_login_new_user_and_trying_refresh_token(hass, aiohttp_client): assert resp.status == 200 tokens = await resp.json() - assert hass.auth.async_get_access_token(tokens['access_token']) is not None + assert ( + await hass.auth.async_validate_access_token(tokens['access_token']) + is not None + ) # Use refresh token to get more tokens. resp = await client.post('/auth/token', data={ @@ -56,7 +59,10 @@ async def test_login_new_user_and_trying_refresh_token(hass, aiohttp_client): assert resp.status == 200 tokens = await resp.json() assert 'refresh_token' not in tokens - assert hass.auth.async_get_access_token(tokens['access_token']) is not None + assert ( + await hass.auth.async_validate_access_token(tokens['access_token']) + is not None + ) # Test using access token to hit API. resp = await client.get('/api/') @@ -98,7 +104,9 @@ async def test_ws_current_user(hass, hass_ws_client, hass_access_token): } }) - user = hass_access_token.refresh_token.user + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + user = refresh_token.user credential = Credentials(auth_provider_type='homeassistant', auth_provider_id=None, data={}, id='test-id') @@ -169,7 +177,10 @@ async def test_refresh_token_system_generated(hass, aiohttp_client): assert resp.status == 200 tokens = await resp.json() - assert hass.auth.async_get_access_token(tokens['access_token']) is not None + assert ( + await hass.auth.async_validate_access_token(tokens['access_token']) + is not None + ) async def test_refresh_token_different_client_id(hass, aiohttp_client): @@ -208,4 +219,7 @@ async def test_refresh_token_different_client_id(hass, aiohttp_client): assert resp.status == 200 tokens = await resp.json() - assert hass.auth.async_get_access_token(tokens['access_token']) is not None + assert ( + await hass.auth.async_validate_access_token(tokens['access_token']) + is not None + ) diff --git a/tests/components/auth/test_init_link_user.py b/tests/components/auth/test_init_link_user.py index 13515db87fa..e209e0ee856 100644 --- a/tests/components/auth/test_init_link_user.py +++ b/tests/components/auth/test_init_link_user.py @@ -52,7 +52,7 @@ async def async_get_code(hass, aiohttp_client): 'user': user, 'code': step['result'], 'client': client, - 'access_token': access_token.token, + 'access_token': access_token, } diff --git a/tests/components/config/test_auth.py b/tests/components/config/test_auth.py index fe8f351955f..cd04eedf08e 100644 --- a/tests/components/config/test_auth.py +++ b/tests/components/config/test_auth.py @@ -122,11 +122,13 @@ async def test_delete_unable_self_account(hass, hass_ws_client, hass_access_token): """Test we cannot delete our own account.""" client = await hass_ws_client(hass, hass_access_token) + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) await client.send_json({ 'id': 5, 'type': auth_config.WS_TYPE_DELETE, - 'user_id': hass_access_token.refresh_token.user.id, + 'user_id': refresh_token.user.id, }) result = await client.receive_json() @@ -137,7 +139,9 @@ async def test_delete_unable_self_account(hass, hass_ws_client, async def test_delete_unknown_user(hass, hass_ws_client, hass_access_token): """Test we cannot delete an unknown user.""" client = await hass_ws_client(hass, hass_access_token) - hass_access_token.refresh_token.user.is_owner = True + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + refresh_token.user.is_owner = True await client.send_json({ 'id': 5, @@ -153,7 +157,9 @@ async def test_delete_unknown_user(hass, hass_ws_client, hass_access_token): async def test_delete(hass, hass_ws_client, hass_access_token): """Test delete command works.""" client = await hass_ws_client(hass, hass_access_token) - hass_access_token.refresh_token.user.is_owner = True + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + refresh_token.user.is_owner = True test_user = MockUser( id='efg', ).add_to_hass(hass) @@ -174,7 +180,9 @@ async def test_delete(hass, hass_ws_client, hass_access_token): async def test_create(hass, hass_ws_client, hass_access_token): """Test create command works.""" client = await hass_ws_client(hass, hass_access_token) - hass_access_token.refresh_token.user.is_owner = True + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + refresh_token.user.is_owner = True assert len(await hass.auth.async_get_users()) == 1 diff --git a/tests/components/config/test_auth_provider_homeassistant.py b/tests/components/config/test_auth_provider_homeassistant.py index cd2cbc44539..a374083c2ab 100644 --- a/tests/components/config/test_auth_provider_homeassistant.py +++ b/tests/components/config/test_auth_provider_homeassistant.py @@ -9,7 +9,7 @@ from tests.common import MockUser, register_auth_provider @pytest.fixture(autouse=True) -def setup_config(hass, aiohttp_client): +def setup_config(hass): """Fixture that sets up the auth provider homeassistant module.""" hass.loop.run_until_complete(register_auth_provider(hass, { 'type': 'homeassistant' @@ -22,7 +22,9 @@ async def test_create_auth_system_generated_user(hass, hass_access_token, """Test we can't add auth to system generated users.""" system_user = MockUser(system_generated=True).add_to_hass(hass) client = await hass_ws_client(hass, hass_access_token) - hass_access_token.refresh_token.user.is_owner = True + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + refresh_token.user.is_owner = True await client.send_json({ 'id': 5, @@ -47,7 +49,9 @@ async def test_create_auth_unknown_user(hass_ws_client, hass, hass_access_token): """Test create pointing at unknown user.""" client = await hass_ws_client(hass, hass_access_token) - hass_access_token.refresh_token.user.is_owner = True + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + refresh_token.user.is_owner = True await client.send_json({ 'id': 5, @@ -86,7 +90,9 @@ async def test_create_auth(hass, hass_ws_client, hass_access_token, """Test create auth command works.""" client = await hass_ws_client(hass, hass_access_token) user = MockUser().add_to_hass(hass) - hass_access_token.refresh_token.user.is_owner = True + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + refresh_token.user.is_owner = True assert len(user.credentials) == 0 @@ -117,7 +123,9 @@ async def test_create_auth_duplicate_username(hass, hass_ws_client, """Test we can't create auth with a duplicate username.""" client = await hass_ws_client(hass, hass_access_token) user = MockUser().add_to_hass(hass) - hass_access_token.refresh_token.user.is_owner = True + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + refresh_token.user.is_owner = True hass_storage[prov_ha.STORAGE_KEY] = { 'version': 1, @@ -145,7 +153,9 @@ async def test_delete_removes_just_auth(hass_ws_client, hass, hass_storage, hass_access_token): """Test deleting an auth without being connected to a user.""" client = await hass_ws_client(hass, hass_access_token) - hass_access_token.refresh_token.user.is_owner = True + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + refresh_token.user.is_owner = True hass_storage[prov_ha.STORAGE_KEY] = { 'version': 1, @@ -171,7 +181,9 @@ async def test_delete_removes_credential(hass, hass_ws_client, hass_access_token, hass_storage): """Test deleting auth that is connected to a user.""" client = await hass_ws_client(hass, hass_access_token) - hass_access_token.refresh_token.user.is_owner = True + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + refresh_token.user.is_owner = True user = MockUser().add_to_hass(hass) user.credentials.append( @@ -216,7 +228,9 @@ async def test_delete_requires_owner(hass, hass_ws_client, hass_access_token): async def test_delete_unknown_auth(hass, hass_ws_client, hass_access_token): """Test trying to delete an unknown auth username.""" client = await hass_ws_client(hass, hass_access_token) - hass_access_token.refresh_token.user.is_owner = True + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + refresh_token.user.is_owner = True await client.send_json({ 'id': 5, @@ -240,7 +254,9 @@ async def test_change_password(hass, hass_ws_client, hass_access_token): 'username': 'test-user' }) - user = hass_access_token.refresh_token.user + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + user = refresh_token.user await hass.auth.async_link_user(user, credentials) client = await hass_ws_client(hass, hass_access_token) @@ -268,7 +284,9 @@ async def test_change_password_wrong_pw(hass, hass_ws_client, 'username': 'test-user' }) - user = hass_access_token.refresh_token.user + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + user = refresh_token.user await hass.auth.async_link_user(user, credentials) client = await hass_ws_client(hass, hass_access_token) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 5f6a17a4101..bb9b643296e 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -28,7 +28,7 @@ def hass_ws_client(aiohttp_client): await websocket.send_json({ 'type': websocket_api.TYPE_AUTH, - 'access_token': access_token.token + 'access_token': access_token }) auth_ok = await websocket.receive_json() diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index b1975669731..4fd59dd3f7a 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -106,7 +106,11 @@ async def test_setup_api_push_api_data_default(hass, aioclient_mock, ) assert hassio_user is not None assert hassio_user.system_generated - assert refresh_token in hassio_user.refresh_tokens + for token in hassio_user.refresh_tokens.values(): + if token.token == refresh_token: + break + else: + assert False, 'refresh token not found' async def test_setup_api_push_api_data_no_auth(hass, aioclient_mock, diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index 31cba79a6c8..8e7a62e2e9f 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -156,9 +156,9 @@ async def test_access_with_trusted_ip(app2, aiohttp_client): async def test_auth_active_access_with_access_token_in_header( - app, aiohttp_client, hass_access_token): + hass, app, aiohttp_client, hass_access_token): """Test access with access token in header.""" - token = hass_access_token.token + token = hass_access_token setup_auth(app, [], True, api_password=None) client = await aiohttp_client(app) @@ -182,7 +182,9 @@ async def test_auth_active_access_with_access_token_in_header( '/', headers={'Authorization': 'BEARER {}'.format(token)}) assert req.status == 401 - hass_access_token.refresh_token.user.is_active = False + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + refresh_token.user.is_active = False req = await client.get( '/', headers={'Authorization': 'Bearer {}'.format(token)}) assert req.status == 401 diff --git a/tests/components/test_api.py b/tests/components/test_api.py index 09dc27e97c1..2be1168b86a 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -448,13 +448,15 @@ async def test_api_fire_event_context(hass, mock_api_client, await mock_api_client.post( const.URL_API_EVENTS_EVENT.format("test.event"), headers={ - 'authorization': 'Bearer {}'.format(hass_access_token.token) + 'authorization': 'Bearer {}'.format(hass_access_token) }) await hass.async_block_till_done() + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + assert len(test_value) == 1 - assert test_value[0].context.user_id == \ - hass_access_token.refresh_token.user.id + assert test_value[0].context.user_id == refresh_token.user.id async def test_api_call_service_context(hass, mock_api_client, @@ -465,12 +467,15 @@ async def test_api_call_service_context(hass, mock_api_client, await mock_api_client.post( '/api/services/test_domain/test_service', headers={ - 'authorization': 'Bearer {}'.format(hass_access_token.token) + 'authorization': 'Bearer {}'.format(hass_access_token) }) await hass.async_block_till_done() + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + assert len(calls) == 1 - assert calls[0].context.user_id == hass_access_token.refresh_token.user.id + assert calls[0].context.user_id == refresh_token.user.id async def test_api_set_state_context(hass, mock_api_client, hass_access_token): @@ -481,8 +486,11 @@ async def test_api_set_state_context(hass, mock_api_client, hass_access_token): 'state': 'on' }, headers={ - 'authorization': 'Bearer {}'.format(hass_access_token.token) + 'authorization': 'Bearer {}'.format(hass_access_token) }) + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + state = hass.states.get('light.kitchen') - assert state.context.user_id == hass_access_token.refresh_token.user.id + assert state.context.user_id == refresh_token.user.id diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py index 1fac1af9f64..199a9d804f8 100644 --- a/tests/components/test_websocket_api.py +++ b/tests/components/test_websocket_api.py @@ -334,7 +334,7 @@ async def test_auth_active_with_token(hass, aiohttp_client, hass_access_token): await ws.send_json({ 'type': wapi.TYPE_AUTH, - 'access_token': hass_access_token.token + 'access_token': hass_access_token }) auth_msg = await ws.receive_json() @@ -344,7 +344,9 @@ async def test_auth_active_with_token(hass, aiohttp_client, hass_access_token): async def test_auth_active_user_inactive(hass, aiohttp_client, hass_access_token): """Test authenticating with a token.""" - hass_access_token.refresh_token.user.is_active = False + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + refresh_token.user.is_active = False assert await async_setup_component(hass, 'websocket_api', { 'http': { 'api_password': API_PASSWORD @@ -361,7 +363,7 @@ async def test_auth_active_user_inactive(hass, aiohttp_client, await ws.send_json({ 'type': wapi.TYPE_AUTH, - 'access_token': hass_access_token.token + 'access_token': hass_access_token }) auth_msg = await ws.receive_json() @@ -465,7 +467,7 @@ async def test_call_service_context_with_user(hass, aiohttp_client, await ws.send_json({ 'type': wapi.TYPE_AUTH, - 'access_token': hass_access_token.token + 'access_token': hass_access_token }) auth_msg = await ws.receive_json() @@ -484,12 +486,15 @@ async def test_call_service_context_with_user(hass, aiohttp_client, msg = await ws.receive_json() assert msg['success'] + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + assert len(calls) == 1 call = calls[0] assert call.domain == 'domain_test' assert call.service == 'test_service' assert call.data == {'hello': 'world'} - assert call.context.user_id == hass_access_token.refresh_token.user.id + assert call.context.user_id == refresh_token.user.id async def test_call_service_context_no_user(hass, aiohttp_client): From 1777270aa2044f0468b877ceee2c3861e463a921 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 14 Aug 2018 22:02:01 +0200 Subject: [PATCH 14/37] Pin crypto (#15978) * Pin crypto * Fix PyJWT import once --- homeassistant/components/notify/html5.py | 2 +- homeassistant/package_constraints.txt | 1 + requirements_all.txt | 4 +--- requirements_test_all.txt | 3 --- setup.py | 2 ++ 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/notify/html5.py b/homeassistant/components/notify/html5.py index e280aa67e40..1ed50472004 100644 --- a/homeassistant/components/notify/html5.py +++ b/homeassistant/components/notify/html5.py @@ -26,7 +26,7 @@ from homeassistant.const import ( from homeassistant.helpers import config_validation as cv from homeassistant.util import ensure_unique_string -REQUIREMENTS = ['pywebpush==1.6.0', 'PyJWT==1.6.0'] +REQUIREMENTS = ['pywebpush==1.6.0'] DEPENDENCIES = ['frontend'] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3aa1e3643c6..26628d7fe62 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,6 +5,7 @@ attrs==18.1.0 certifi>=2018.04.16 jinja2>=2.10 PyJWT==1.6.4 +cryptography==2.3.1 pip>=8.0.3 pytz>=2018.04 pyyaml>=3.13,<4 diff --git a/requirements_all.txt b/requirements_all.txt index 3f50e50d19a..5289db61f62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,6 +6,7 @@ attrs==18.1.0 certifi>=2018.04.16 jinja2>=2.10 PyJWT==1.6.4 +cryptography==2.3.1 pip>=8.0.3 pytz>=2018.04 pyyaml>=3.13,<4 @@ -39,9 +40,6 @@ Mastodon.py==1.3.1 # homeassistant.components.isy994 PyISY==1.1.0 -# homeassistant.components.notify.html5 -PyJWT==1.6.0 - # homeassistant.components.sensor.mvglive PyMVGLive==1.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ffc55c23210..4115fcfcb3f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -21,9 +21,6 @@ requests_mock==1.5.2 # homeassistant.components.homekit HAP-python==2.2.2 -# homeassistant.components.notify.html5 -PyJWT==1.6.0 - # homeassistant.components.sensor.rmvtransport PyRMVtransport==0.0.7 diff --git a/setup.py b/setup.py index bd1e70aa8ae..7484dc286e6 100755 --- a/setup.py +++ b/setup.py @@ -39,6 +39,8 @@ REQUIRES = [ 'certifi>=2018.04.16', 'jinja2>=2.10', 'PyJWT==1.6.4', + # PyJWT has loose dependency. We want the latest one. + 'cryptography==2.3.1', 'pip>=8.0.3', 'pytz>=2018.04', 'pyyaml>=3.13,<4', From e64e84ad7af2bf5578e08977732a576e4f9a48e5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 14 Aug 2018 22:06:57 +0200 Subject: [PATCH 15/37] Bumped version to 0.76.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 52175c2b4e9..200b58461b4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 76 -PATCH_VERSION = '0b1' +PATCH_VERSION = '0b2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 4035880003243abcb6d4a9e3623c3f3a1b8d9773 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 15 Aug 2018 10:50:11 +0200 Subject: [PATCH 16/37] Update translations --- .../components/deconz/.translations/de.json | 3 ++- .../homematicip_cloud/.translations/de.json | 4 +++- .../homematicip_cloud/.translations/no.json | 2 +- .../homematicip_cloud/.translations/pt.json | 2 +- homeassistant/components/hue/.translations/de.json | 2 +- homeassistant/components/hue/.translations/ro.json | 4 ++++ homeassistant/components/nest/.translations/de.json | 2 ++ .../components/sensor/.translations/moon.ar.json | 6 ++++++ .../components/sensor/.translations/moon.ca.json | 12 ++++++++++++ .../components/sensor/.translations/moon.de.json | 12 ++++++++++++ .../components/sensor/.translations/moon.en.json | 12 ++++++++++++ .../components/sensor/.translations/moon.es-419.json | 12 ++++++++++++ .../components/sensor/.translations/moon.fr.json | 12 ++++++++++++ .../components/sensor/.translations/moon.ko.json | 12 ++++++++++++ .../components/sensor/.translations/moon.nl.json | 12 ++++++++++++ .../components/sensor/.translations/moon.no.json | 12 ++++++++++++ .../components/sensor/.translations/moon.ru.json | 12 ++++++++++++ .../components/sensor/.translations/moon.sl.json | 12 ++++++++++++ .../sensor/.translations/moon.zh-Hans.json | 12 ++++++++++++ .../sensor/.translations/moon.zh-Hant.json | 12 ++++++++++++ 20 files changed, 164 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/sensor/.translations/moon.ar.json create mode 100644 homeassistant/components/sensor/.translations/moon.ca.json create mode 100644 homeassistant/components/sensor/.translations/moon.de.json create mode 100644 homeassistant/components/sensor/.translations/moon.en.json create mode 100644 homeassistant/components/sensor/.translations/moon.es-419.json create mode 100644 homeassistant/components/sensor/.translations/moon.fr.json create mode 100644 homeassistant/components/sensor/.translations/moon.ko.json create mode 100644 homeassistant/components/sensor/.translations/moon.nl.json create mode 100644 homeassistant/components/sensor/.translations/moon.no.json create mode 100644 homeassistant/components/sensor/.translations/moon.ru.json create mode 100644 homeassistant/components/sensor/.translations/moon.sl.json create mode 100644 homeassistant/components/sensor/.translations/moon.zh-Hans.json create mode 100644 homeassistant/components/sensor/.translations/moon.zh-Hant.json diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json index b09b7e15b31..51b496906a2 100644 --- a/homeassistant/components/deconz/.translations/de.json +++ b/homeassistant/components/deconz/.translations/de.json @@ -24,7 +24,8 @@ "data": { "allow_clip_sensor": "Import virtueller Sensoren zulassen", "allow_deconz_groups": "Import von deCONZ-Gruppen zulassen" - } + }, + "title": "Weitere Konfigurationsoptionen f\u00fcr deCONZ" } }, "title": "deCONZ Zigbee Gateway" diff --git a/homeassistant/components/homematicip_cloud/.translations/de.json b/homeassistant/components/homematicip_cloud/.translations/de.json index 8e4130a3251..61a9bd6eb40 100644 --- a/homeassistant/components/homematicip_cloud/.translations/de.json +++ b/homeassistant/components/homematicip_cloud/.translations/de.json @@ -17,9 +17,11 @@ "hapid": "Accesspoint ID (SGTIN)", "name": "Name (optional, wird als Pr\u00e4fix f\u00fcr alle Ger\u00e4te verwendet)", "pin": "PIN Code (optional)" - } + }, + "title": "HometicIP Accesspoint ausw\u00e4hlen" }, "link": { + "description": "Dr\u00fccken Sie den blauen Taster auf dem Accesspoint, sowie den Senden Button um HomematicIP mit Home Assistant zu verbinden.\n\n![Position des Tasters auf dem AP](/static/images/config_flows/config_homematicip_cloud.png)", "title": "Verkn\u00fcpfe den Accesspoint" } }, diff --git a/homeassistant/components/homematicip_cloud/.translations/no.json b/homeassistant/components/homematicip_cloud/.translations/no.json index 7e164abd3bb..650c921af31 100644 --- a/homeassistant/components/homematicip_cloud/.translations/no.json +++ b/homeassistant/components/homematicip_cloud/.translations/no.json @@ -22,7 +22,7 @@ }, "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)", - "title": "Link Tilgangspunkt" + "title": "Link tilgangspunkt" } }, "title": "HomematicIP Sky" diff --git a/homeassistant/components/homematicip_cloud/.translations/pt.json b/homeassistant/components/homematicip_cloud/.translations/pt.json index ef742e2ce5e..2266e83ac44 100644 --- a/homeassistant/components/homematicip_cloud/.translations/pt.json +++ b/homeassistant/components/homematicip_cloud/.translations/pt.json @@ -21,7 +21,7 @@ "title": "Escolher ponto de acesso HomematicIP" }, "link": { - "description": "Pressione o bot\u00e3o azul no accesspoint e o bot\u00e3o enviar para registrar HomematicIP com Home Assistant.\n\n! [Localiza\u00e7\u00e3o do bot\u00e3o na ponte] (/ static/images/config_flows/config_homematicip_cloud.png)", + "description": "Pressione o bot\u00e3o azul no ponto de acesso e o bot\u00e3o enviar para registrar HomematicIP com o Home Assistant.\n\n![Localiza\u00e7\u00e3o do bot\u00e3o na ponte](/ static/images/config_flows/config_homematicip_cloud.png)", "title": "Associar ponto de acesso" } }, diff --git a/homeassistant/components/hue/.translations/de.json b/homeassistant/components/hue/.translations/de.json index dc0968dc88a..a0bd50d8514 100644 --- a/homeassistant/components/hue/.translations/de.json +++ b/homeassistant/components/hue/.translations/de.json @@ -24,6 +24,6 @@ "title": "Hub verbinden" } }, - "title": "" + "title": "Philips Hue" } } \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/ro.json b/homeassistant/components/hue/.translations/ro.json index 91541edcc7d..69cee1198d3 100644 --- a/homeassistant/components/hue/.translations/ro.json +++ b/homeassistant/components/hue/.translations/ro.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "all_configured": "Toate pun\u021bile Philips Hue sunt deja configurate", + "discover_timeout": "Imposibil de descoperit podurile Hue" + }, "error": { "linking": "A ap\u0103rut o eroare de leg\u0103tur\u0103 necunoscut\u0103.", "register_failed": "Nu a reu\u0219it \u00eenregistrarea, \u00eencerca\u021bi din nou" diff --git a/homeassistant/components/nest/.translations/de.json b/homeassistant/components/nest/.translations/de.json index 32c72ef7d96..86b50ab3c10 100644 --- a/homeassistant/components/nest/.translations/de.json +++ b/homeassistant/components/nest/.translations/de.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_setup": "Sie k\u00f6nnen nur ein einziges Nest-Konto konfigurieren.", + "authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL", + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL", "no_flows": "Sie m\u00fcssen Nest konfigurieren, bevor Sie sich authentifizieren k\u00f6nnen. [Bitte lesen Sie die Anweisungen] (https://www.home-assistant.io/components/nest/)." }, "error": { diff --git a/homeassistant/components/sensor/.translations/moon.ar.json b/homeassistant/components/sensor/.translations/moon.ar.json new file mode 100644 index 00000000000..94af741f5f4 --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.ar.json @@ -0,0 +1,6 @@ +{ + "state": { + "first_quarter": "\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644", + "full_moon": "\u0627\u0644\u0642\u0645\u0631 \u0627\u0644\u0643\u0627\u0645\u0644" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.ca.json b/homeassistant/components/sensor/.translations/moon.ca.json new file mode 100644 index 00000000000..56eaf8d3b4c --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.ca.json @@ -0,0 +1,12 @@ +{ + "state": { + "first_quarter": "Quart creixent", + "full_moon": "Lluna plena", + "last_quarter": "Quart minvant", + "new_moon": "Lluna nova", + "waning_crescent": "Lluna vella minvant", + "waning_gibbous": "Gibosa minvant", + "waxing_crescent": "Lluna nova visible", + "waxing_gibbous": "Gibosa creixent" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.de.json b/homeassistant/components/sensor/.translations/moon.de.json new file mode 100644 index 00000000000..aebca53ec4d --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.de.json @@ -0,0 +1,12 @@ +{ + "state": { + "first_quarter": "Erstes Viertel", + "full_moon": "Vollmond", + "last_quarter": "Letztes Viertel", + "new_moon": "Neumond", + "waning_crescent": "Abnehmende Sichel", + "waning_gibbous": "Drittes Viertel", + "waxing_crescent": " Zunehmende Sichel", + "waxing_gibbous": "Zweites Viertel" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.en.json b/homeassistant/components/sensor/.translations/moon.en.json new file mode 100644 index 00000000000..587b9496114 --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.en.json @@ -0,0 +1,12 @@ +{ + "state": { + "first_quarter": "First quarter", + "full_moon": "Full moon", + "last_quarter": "Last quarter", + "new_moon": "New moon", + "waning_crescent": "Waning crescent", + "waning_gibbous": "Waning gibbous", + "waxing_crescent": "Waxing crescent", + "waxing_gibbous": "Waxing gibbous" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.es-419.json b/homeassistant/components/sensor/.translations/moon.es-419.json new file mode 100644 index 00000000000..71cfab736cb --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.es-419.json @@ -0,0 +1,12 @@ +{ + "state": { + "first_quarter": "Cuarto creciente", + "full_moon": "Luna llena", + "last_quarter": "Cuarto menguante", + "new_moon": "Luna nueva", + "waning_crescent": "Luna menguante", + "waning_gibbous": "Luna menguante gibosa", + "waxing_crescent": "Luna creciente", + "waxing_gibbous": "Luna creciente gibosa" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.fr.json b/homeassistant/components/sensor/.translations/moon.fr.json new file mode 100644 index 00000000000..fac2b654a46 --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.fr.json @@ -0,0 +1,12 @@ +{ + "state": { + "first_quarter": "Premier quartier", + "full_moon": "Pleine lune", + "last_quarter": "Dernier quartier", + "new_moon": "Nouvelle lune", + "waning_crescent": "Dernier croissant", + "waning_gibbous": "Gibbeuse d\u00e9croissante", + "waxing_crescent": "Premier croissant", + "waxing_gibbous": "Gibbeuse croissante" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.ko.json b/homeassistant/components/sensor/.translations/moon.ko.json new file mode 100644 index 00000000000..7e62250b892 --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.ko.json @@ -0,0 +1,12 @@ +{ + "state": { + "first_quarter": "\ubc18\ub2ec(\ucc28\uc624\ub974\ub294)", + "full_moon": "\ubcf4\ub984\ub2ec", + "last_quarter": "\ubc18\ub2ec(\uc904\uc5b4\ub4dc\ub294)", + "new_moon": "\uc0ad\uc6d4", + "waning_crescent": "\uadf8\ubbd0\ub2ec", + "waning_gibbous": "\ud558\ud604\ub2ec", + "waxing_crescent": "\ucd08\uc2b9\ub2ec", + "waxing_gibbous": "\uc0c1\ud604\ub2ec" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.nl.json b/homeassistant/components/sensor/.translations/moon.nl.json new file mode 100644 index 00000000000..5e78d429b9f --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.nl.json @@ -0,0 +1,12 @@ +{ + "state": { + "first_quarter": "Eerste kwartier", + "full_moon": "Volle maan", + "last_quarter": "Laatste kwartier", + "new_moon": "Nieuwe maan", + "waning_crescent": "Krimpende, sikkelvormige maan", + "waning_gibbous": "Krimpende, vooruitspringende maan", + "waxing_crescent": "Wassende, sikkelvormige maan", + "waxing_gibbous": "Wassende, vooruitspringende maan" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.no.json b/homeassistant/components/sensor/.translations/moon.no.json new file mode 100644 index 00000000000..104412c90ba --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.no.json @@ -0,0 +1,12 @@ +{ + "state": { + "first_quarter": "F\u00f8rste kvartdel", + "full_moon": "Fullm\u00e5ne", + "last_quarter": "Siste kvartdel", + "new_moon": "Nym\u00e5ne", + "waning_crescent": "Minkende halvm\u00e5ne", + "waning_gibbous": "Minkende trekvartm\u00e5ne", + "waxing_crescent": "Voksende halvm\u00e5ne", + "waxing_gibbous": "Voksende trekvartm\u00e5ne" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.ru.json b/homeassistant/components/sensor/.translations/moon.ru.json new file mode 100644 index 00000000000..6db932a1aed --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.ru.json @@ -0,0 +1,12 @@ +{ + "state": { + "first_quarter": "\u041f\u0435\u0440\u0432\u0430\u044f \u0447\u0435\u0442\u0432\u0435\u0440\u0442\u044c", + "full_moon": "\u041f\u043e\u043b\u043d\u043e\u043b\u0443\u043d\u0438\u0435", + "last_quarter": "\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u044f\u044f \u0447\u0435\u0442\u0432\u0435\u0440\u0442\u044c", + "new_moon": "\u041d\u043e\u0432\u043e\u043b\u0443\u043d\u0438\u0435", + "waning_crescent": "\u0421\u0442\u0430\u0440\u0430\u044f \u043b\u0443\u043d\u0430", + "waning_gibbous": "\u0423\u0431\u044b\u0432\u0430\u044e\u0449\u0430\u044f \u043b\u0443\u043d\u0430", + "waxing_crescent": "\u041c\u043e\u043b\u043e\u0434\u0430\u044f \u043b\u0443\u043d\u0430", + "waxing_gibbous": "\u041f\u0440\u0438\u0431\u044b\u0432\u0430\u044e\u0449\u0430\u044f \u043b\u0443\u043d\u0430" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.sl.json b/homeassistant/components/sensor/.translations/moon.sl.json new file mode 100644 index 00000000000..41e873e4def --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.sl.json @@ -0,0 +1,12 @@ +{ + "state": { + "first_quarter": "Prvi krajec", + "full_moon": "Polna luna", + "last_quarter": "Zadnji krajec", + "new_moon": "Mlaj", + "waning_crescent": "Zadnji izbo\u010dec", + "waning_gibbous": "Zadnji srpec", + "waxing_crescent": " Prvi izbo\u010dec", + "waxing_gibbous": "Prvi srpec" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.zh-Hans.json b/homeassistant/components/sensor/.translations/moon.zh-Hans.json new file mode 100644 index 00000000000..22ab0d49f62 --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.zh-Hans.json @@ -0,0 +1,12 @@ +{ + "state": { + "first_quarter": "\u4e0a\u5f26\u6708", + "full_moon": "\u6ee1\u6708", + "last_quarter": "\u4e0b\u5f26\u6708", + "new_moon": "\u65b0\u6708", + "waning_crescent": "\u6b8b\u6708", + "waning_gibbous": "\u4e8f\u51f8\u6708", + "waxing_crescent": "\u5ce8\u7709\u6708", + "waxing_gibbous": "\u76c8\u51f8\u6708" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.zh-Hant.json b/homeassistant/components/sensor/.translations/moon.zh-Hant.json new file mode 100644 index 00000000000..9cf4aad011e --- /dev/null +++ b/homeassistant/components/sensor/.translations/moon.zh-Hant.json @@ -0,0 +1,12 @@ +{ + "state": { + "first_quarter": "\u4e0a\u5f26\u6708", + "full_moon": "\u6eff\u6708", + "last_quarter": "\u4e0b\u5f26\u6708", + "new_moon": "\u65b0\u6708", + "waning_crescent": "\u6b98\u6708", + "waning_gibbous": "\u8667\u51f8\u6708", + "waxing_crescent": "\u86fe\u7709\u6708", + "waxing_gibbous": "\u76c8\u51f8\u6708" + } +} \ No newline at end of file From 2306d14b5dd4d5f115c8e4b7ac233287edb66c7f Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Tue, 14 Aug 2018 23:09:19 -0700 Subject: [PATCH 17/37] Teak mqtt error message for 0.76 release (#15983) --- homeassistant/components/mqtt/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 70d4d7aa5d7..19bacbc8d4c 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -354,11 +354,11 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: if (conf.get(CONF_PASSWORD) is None and config.get('http') is not None and config['http'].get('api_password') is not None): - _LOGGER.error("Starting from 0.77, embedded MQTT broker doesn't" - " use api_password as default password any more." - " Please set password configuration. See https://" - "home-assistant.io/docs/mqtt/broker#embedded-broker" - " for details") + _LOGGER.error( + "Starting from release 0.76, the embedded MQTT broker does not" + " use api_password as default password anymore. Please set" + " password configuration. See https://home-assistant.io/docs/" + "mqtt/broker#embedded-broker for details") return False broker_config = await _async_setup_server(hass, config) From f8051a56987dc6569640c196406d40b52c863bc6 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Wed, 15 Aug 2018 00:56:05 -0700 Subject: [PATCH 18/37] Fix 0.76 beta2 hassio token issue (#15987) --- homeassistant/components/hassio/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 13c486533d9..e0356017e3e 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -178,7 +178,7 @@ def async_setup(hass, config): refresh_token = None if 'hassio_user' in data: user = yield from hass.auth.async_get_user(data['hassio_user']) - if user: + if user and user.refresh_tokens: refresh_token = list(user.refresh_tokens.values())[0] if refresh_token is None: From 6da0ae4d231052aad4c5f3e1677e3df050c73fdc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 15 Aug 2018 10:55:03 +0200 Subject: [PATCH 19/37] Bumped version to 0.76.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 200b58461b4..5e7d6f9585b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 76 -PATCH_VERSION = '0b2' +PATCH_VERSION = '0b3' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 11eb29f520191d20a9b777d0f30c434d77bd0677 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 16 Aug 2018 14:21:43 +0200 Subject: [PATCH 20/37] Bump frontend to 20180816.0 --- homeassistant/components/frontend/__init__.py | 2 +- homeassistant/components/map.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 41cfdd3edd8..c3c742f43b1 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180813.0'] +REQUIREMENTS = ['home-assistant-frontend==20180816.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/homeassistant/components/map.py b/homeassistant/components/map.py index 30cb00af69e..c0184239a1a 100644 --- a/homeassistant/components/map.py +++ b/homeassistant/components/map.py @@ -10,5 +10,5 @@ DOMAIN = 'map' async def async_setup(hass, config): """Register the built-in map panel.""" await hass.components.frontend.async_register_built_in_panel( - 'map', 'map', 'mdi:account-location') + 'map', 'map', 'hass:account-location') return True diff --git a/requirements_all.txt b/requirements_all.txt index 5289db61f62..475f19f3dd0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -432,7 +432,7 @@ hole==0.3.0 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180813.0 +home-assistant-frontend==20180816.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4115fcfcb3f..0958b6d7280 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180813.0 +home-assistant-frontend==20180816.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From d540a084dd07e01b7170e1be1843f87607d61526 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 16 Aug 2018 14:19:42 +0200 Subject: [PATCH 21/37] Fix mysensors connection task blocking setup (#15938) * Fix mysensors connection task blocking setup * Schedule the connection task without having the core track the task to avoid blocking setup. * Cancel the connection task, if not cancelled already, when home assistant stops. * Use done instead of cancelled --- homeassistant/components/mysensors/gateway.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index 8c80604d188..88725e67940 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -186,12 +186,16 @@ def _discover_mysensors_platform(hass, platform, new_devices): async def _gw_start(hass, gateway): """Start the gateway.""" + # Don't use hass.async_create_task to avoid holding up setup indefinitely. + connect_task = hass.loop.create_task(gateway.start()) + @callback def gw_stop(event): """Trigger to stop the gateway.""" hass.async_add_job(gateway.stop()) + if not connect_task.done(): + connect_task.cancel() - await gateway.start() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gw_stop) if gateway.device == 'mqtt': # Gatways connected via mqtt doesn't send gateway ready message. From 2469bc7e2ed6b6b64e5fc8071e631b0c726f3b65 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 16 Aug 2018 13:46:43 +0200 Subject: [PATCH 22/37] Fix Nest async from sync (#15997) --- homeassistant/components/nest/__init__.py | 43 +++++++++++++---------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index de9783ba931..d25b94bbc17 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -4,10 +4,10 @@ Support for Nest devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/nest/ """ -from concurrent.futures import ThreadPoolExecutor import logging import socket from datetime import datetime, timedelta +import threading import voluptuous as vol @@ -16,8 +16,9 @@ from homeassistant.const import ( CONF_STRUCTURE, CONF_FILENAME, CONF_BINARY_SENSORS, CONF_SENSORS, CONF_MONITORED_CONDITIONS, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_send, \ +from homeassistant.helpers.dispatcher import dispatcher_send, \ async_dispatcher_connect from homeassistant.helpers.entity import Entity @@ -71,24 +72,25 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -async def async_nest_update_event_broker(hass, nest): +def nest_update_event_broker(hass, nest): """ Dispatch SIGNAL_NEST_UPDATE to devices when nest stream API received data. - nest.update_event.wait will block the thread in most of time, - so specific an executor to save default thread pool. + Runs in its own thread. """ _LOGGER.debug("listening nest.update_event") - with ThreadPoolExecutor(max_workers=1) as executor: - while True: - await hass.loop.run_in_executor(executor, nest.update_event.wait) - if hass.is_running: - nest.update_event.clear() - _LOGGER.debug("dispatching nest data update") - async_dispatcher_send(hass, SIGNAL_NEST_UPDATE) - else: - _LOGGER.debug("stop listening nest.update_event") - return + + while hass.is_running: + nest.update_event.wait() + + if not hass.is_running: + break + + nest.update_event.clear() + _LOGGER.debug("dispatching nest data update") + dispatcher_send(hass, SIGNAL_NEST_UPDATE) + + _LOGGER.debug("stop listening nest.update_event") async def async_setup(hass, config): @@ -167,16 +169,21 @@ async def async_setup_entry(hass, entry): hass.services.async_register( DOMAIN, 'set_mode', set_mode, schema=AWAY_SCHEMA) + @callback def start_up(event): """Start Nest update event listener.""" - hass.async_add_job(async_nest_update_event_broker, hass, nest) + threading.Thread( + name='Nest update listener', + target=nest_update_event_broker, + args=(hass, nest) + ).start() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_up) + @callback def shut_down(event): """Stop Nest update event listener.""" - if nest: - nest.update_event.set() + nest.update_event.set() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shut_down) From 5eccfc2604ff72580a9bec6a0bab789ff4a09b9f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 16 Aug 2018 14:26:52 +0200 Subject: [PATCH 23/37] Bumped version to 0.76.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5e7d6f9585b..8914a6ba76c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 76 -PATCH_VERSION = '0b3' +PATCH_VERSION = '0b4' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From e41ce1d6ecb8c2b4497a1f6e1eda3b13d596fa88 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 16 Aug 2018 22:15:39 +0200 Subject: [PATCH 24/37] Bump frontend to 20180816.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 c3c742f43b1..40fb6056684 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180816.0'] +REQUIREMENTS = ['home-assistant-frontend==20180816.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 475f19f3dd0..aad64d205f3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -432,7 +432,7 @@ hole==0.3.0 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180816.0 +home-assistant-frontend==20180816.1 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0958b6d7280..8b153494025 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180816.0 +home-assistant-frontend==20180816.1 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 061859cc4decd3745724fc35222d71af3c180871 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Thu, 16 Aug 2018 22:42:11 +0200 Subject: [PATCH 25/37] Fix message "Updating dlna_dmr media_player took longer than ..." (#16005) --- homeassistant/components/media_player/dlna_dmr.py | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/dlna_dmr.py b/homeassistant/components/media_player/dlna_dmr.py index 9b6beb83341..c40e3ed0ca9 100644 --- a/homeassistant/components/media_player/dlna_dmr.py +++ b/homeassistant/components/media_player/dlna_dmr.py @@ -35,7 +35,7 @@ from homeassistant.util import get_local_ip DLNA_DMR_DATA = 'dlna_dmr' REQUIREMENTS = [ - 'async-upnp-client==0.12.2', + 'async-upnp-client==0.12.3', ] DEFAULT_NAME = 'DLNA Digital Media Renderer' @@ -126,7 +126,7 @@ async def async_setup_platform(hass: HomeAssistant, name = config.get(CONF_NAME) elif discovery_info is not None: url = discovery_info['ssdp_description'] - name = discovery_info['name'] + name = discovery_info.get('name') if DLNA_DMR_DATA not in hass.data: hass.data[DLNA_DMR_DATA] = {} diff --git a/requirements_all.txt b/requirements_all.txt index aad64d205f3..d65ef4fce18 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -138,7 +138,7 @@ apns2==0.3.0 asterisk_mbox==0.4.0 # homeassistant.components.media_player.dlna_dmr -async-upnp-client==0.12.2 +async-upnp-client==0.12.3 # homeassistant.components.light.avion # avion==0.7 From 92e26495da9dad9359fcef426e37b5f510f30d8e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 16 Aug 2018 22:41:44 +0200 Subject: [PATCH 26/37] Disable the DLNA component discovery (#16006) --- homeassistant/components/discovery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index b400d1d8885..41cf3791256 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -85,11 +85,11 @@ SERVICE_HANDLERS = { 'volumio': ('media_player', 'volumio'), 'nanoleaf_aurora': ('light', 'nanoleaf_aurora'), 'freebox': ('device_tracker', 'freebox'), - 'dlna_dmr': ('media_player', 'dlna_dmr'), } OPTIONAL_SERVICE_HANDLERS = { SERVICE_HOMEKIT: ('homekit_controller', None), + 'dlna_dmr': ('media_player', 'dlna_dmr'), } CONF_IGNORE = 'ignore' From c09e7e620f092fb400e0d6e3e39feb85e2e4b44c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 16 Aug 2018 23:02:34 +0200 Subject: [PATCH 27/37] Bumped version to 0.76.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8914a6ba76c..7a9dbaff042 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 76 -PATCH_VERSION = '0b4' +PATCH_VERSION = '0b5' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From e4425e6a37eb8a311221de5942f4d8ef20a89443 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 17 Aug 2018 17:23:20 +0200 Subject: [PATCH 28/37] Version 0.76.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7a9dbaff042..5a481e0a8c1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 76 -PATCH_VERSION = '0b5' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 6827256586773d337aedda349037884da52a6c17 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 18 Aug 2018 11:15:33 +0200 Subject: [PATCH 29/37] Bump frontend to 20180818.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 40fb6056684..e17bbad78d1 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180816.1'] +REQUIREMENTS = ['home-assistant-frontend==20180818.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index d65ef4fce18..bcbcd649805 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -432,7 +432,7 @@ hole==0.3.0 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180816.1 +home-assistant-frontend==20180818.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8b153494025..24aa79587ad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180816.1 +home-assistant-frontend==20180818.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 1c8ef4e196eb5930f90f5491d19f53bd4cf8cf98 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 19 Aug 2018 17:22:09 +0200 Subject: [PATCH 30/37] Add forgiving add column (#16057) * Add forgiving add column * Lint --- .../components/recorder/migration.py | 27 ++++++++++++++----- tests/components/recorder/test_migrate.py | 25 +++++++++++++---- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 939985ebfb1..5633830dc51 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -117,7 +117,13 @@ def _drop_index(engine, table_name, index_name): def _add_columns(engine, table_name, columns_def): """Add columns to a table.""" from sqlalchemy import text - from sqlalchemy.exc import SQLAlchemyError + from sqlalchemy.exc import OperationalError + + _LOGGER.info("Adding columns %s to table %s. Note: this can take several " + "minutes on large databases and slow computers. Please " + "be patient!", + ', '.join(column.split(' ')[0] for column in columns_def), + table_name) columns_def = ['ADD COLUMN {}'.format(col_def) for col_def in columns_def] @@ -126,13 +132,22 @@ def _add_columns(engine, table_name, columns_def): table=table_name, columns_def=', '.join(columns_def)))) return - except SQLAlchemyError: - pass + except OperationalError: + # Some engines support adding all columns at once, + # this error is when they dont' + _LOGGER.info('Unable to use quick column add. Adding 1 by 1.') for column_def in columns_def: - engine.execute(text("ALTER TABLE {table} {column_def}".format( - table=table_name, - column_def=column_def))) + try: + engine.execute(text("ALTER TABLE {table} {column_def}".format( + table=table_name, + column_def=column_def))) + except OperationalError as err: + if 'duplicate' not in str(err).lower(): + raise + + _LOGGER.warning('Column %s already exists on %s, continueing', + column_def.split(' ')[0], table_name) def _apply_update(engine, new_version, old_version): diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 5ac9b3adb81..1c48c261372 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -5,11 +5,11 @@ from unittest.mock import patch, call import pytest from sqlalchemy import create_engine +from sqlalchemy.pool import StaticPool from homeassistant.bootstrap import async_setup_component -from homeassistant.components.recorder import wait_connection_ready, migration -from homeassistant.components.recorder.models import SCHEMA_VERSION -from homeassistant.components.recorder.const import DATA_INSTANCE +from homeassistant.components.recorder import ( + wait_connection_ready, migration, const, models) from tests.components.recorder import models_original @@ -37,8 +37,8 @@ def test_schema_update_calls(hass): yield from wait_connection_ready(hass) update.assert_has_calls([ - call(hass.data[DATA_INSTANCE].engine, version+1, 0) for version - in range(0, SCHEMA_VERSION)]) + call(hass.data[const.DATA_INSTANCE].engine, version+1, 0) for version + in range(0, models.SCHEMA_VERSION)]) @asyncio.coroutine @@ -65,3 +65,18 @@ def test_invalid_update(): """Test that an invalid new version raises an exception.""" with pytest.raises(ValueError): migration._apply_update(None, -1, 0) + + +def test_forgiving_add_column(): + """Test that add column will continue if column exists.""" + engine = create_engine( + 'sqlite://', + poolclass=StaticPool + ) + engine.execute('CREATE TABLE hello (id int)') + migration._add_columns(engine, 'hello', [ + 'context_id CHARACTER(36)', + ]) + migration._add_columns(engine, 'hello', [ + 'context_id CHARACTER(36)', + ]) From cb44607e96f7dff29ec18d39119e0e909a6fb7e1 Mon Sep 17 00:00:00 2001 From: huangyupeng Date: Mon, 20 Aug 2018 00:55:10 +0800 Subject: [PATCH 31/37] Tuya fix login problem and add login platform param (#16058) * add a platform param to distinguish different app's account. * fix requirements --- homeassistant/components/tuya.py | 10 ++++++---- requirements_all.txt | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tuya.py b/homeassistant/components/tuya.py index 490c11baad7..33f34164b02 100644 --- a/homeassistant/components/tuya.py +++ b/homeassistant/components/tuya.py @@ -10,14 +10,14 @@ import voluptuous as vol from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD) +from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, CONF_PLATFORM) from homeassistant.helpers import discovery from homeassistant.helpers.dispatcher import ( dispatcher_send, async_dispatcher_connect) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_time_interval -REQUIREMENTS = ['tuyapy==0.1.2'] +REQUIREMENTS = ['tuyapy==0.1.3'] _LOGGER = logging.getLogger(__name__) @@ -45,7 +45,8 @@ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_COUNTRYCODE): cv.string + vol.Required(CONF_COUNTRYCODE): cv.string, + vol.Optional(CONF_PLATFORM, default='tuya'): cv.string, }) }, extra=vol.ALLOW_EXTRA) @@ -58,9 +59,10 @@ def setup(hass, config): username = config[DOMAIN][CONF_USERNAME] password = config[DOMAIN][CONF_PASSWORD] country_code = config[DOMAIN][CONF_COUNTRYCODE] + platform = config[DOMAIN][CONF_PLATFORM] hass.data[DATA_TUYA] = tuya - tuya.init(username, password, country_code) + tuya.init(username, password, country_code, platform) hass.data[DOMAIN] = { 'entities': {} } diff --git a/requirements_all.txt b/requirements_all.txt index bcbcd649805..a2b640cae1e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1389,7 +1389,7 @@ tplink==0.2.1 transmissionrpc==0.11 # homeassistant.components.tuya -tuyapy==0.1.2 +tuyapy==0.1.3 # homeassistant.components.twilio twilio==5.7.0 From 9c3251b5f0d287c95afeef88ed7ed57d3c790026 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 19 Aug 2018 18:56:31 +0200 Subject: [PATCH 32/37] Add notify platforms to loaded components (#16063) --- homeassistant/components/notify/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 13cd6203ed4..4de35d3f850 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -156,6 +156,8 @@ def async_setup(hass, config): DOMAIN, platform_name_slug, async_notify_message, schema=NOTIFY_SERVICE_SCHEMA) + hass.config.components.add('{}.{}'.format(DOMAIN, p_type)) + return True setup_tasks = [async_setup_platform(p_type, p_config) for p_type, p_config From 3be301fac9b213b6b252ec92cffcd1d62d3cc960 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 19 Aug 2018 18:58:21 +0200 Subject: [PATCH 33/37] Bumped version to 0.76.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5a481e0a8c1..855f973554b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 76 -PATCH_VERSION = '0' +PATCH_VERSION = '1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 3c5e62d47eeded341b41cbe769f3e29b7b6f1614 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 19 Aug 2018 18:57:06 +0200 Subject: [PATCH 34/37] Column syntax fix + Add a file if migration in progress (#16061) * Add a file if migration in progress * Warning * Convert message for migration to warning --- .../components/recorder/migration.py | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 5633830dc51..0dff21a5986 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -1,39 +1,53 @@ """Schema migration helpers.""" import logging +import os from .util import session_scope _LOGGER = logging.getLogger(__name__) +PROGRESS_FILE = '.migration_progress' def migrate_schema(instance): """Check if the schema needs to be upgraded.""" from .models import SchemaChanges, SCHEMA_VERSION + progress_path = instance.hass.config.path(PROGRESS_FILE) + with session_scope(session=instance.get_session()) as session: res = session.query(SchemaChanges).order_by( SchemaChanges.change_id.desc()).first() current_version = getattr(res, 'schema_version', None) if current_version == SCHEMA_VERSION: + # Clean up if old migration left file + if os.path.isfile(progress_path): + _LOGGER.warning("Found existing migration file, cleaning up") + os.remove(instance.hass.config.path(PROGRESS_FILE)) return - _LOGGER.debug("Database requires upgrade. Schema version: %s", - current_version) + with open(progress_path, 'w'): + pass + + _LOGGER.warning("Database requires upgrade. Schema version: %s", + current_version) if current_version is None: current_version = _inspect_schema_version(instance.engine, session) _LOGGER.debug("No schema version found. Inspected version: %s", current_version) - for version in range(current_version, SCHEMA_VERSION): - new_version = version + 1 - _LOGGER.info("Upgrading recorder db schema to version %s", - new_version) - _apply_update(instance.engine, new_version, current_version) - session.add(SchemaChanges(schema_version=new_version)) + try: + for version in range(current_version, SCHEMA_VERSION): + new_version = version + 1 + _LOGGER.info("Upgrading recorder db schema to version %s", + new_version) + _apply_update(instance.engine, new_version, current_version) + session.add(SchemaChanges(schema_version=new_version)) - _LOGGER.info("Upgrade to version %s done", new_version) + _LOGGER.info("Upgrade to version %s done", new_version) + finally: + os.remove(instance.hass.config.path(PROGRESS_FILE)) def _create_index(engine, table_name, index_name): @@ -125,7 +139,7 @@ def _add_columns(engine, table_name, columns_def): ', '.join(column.split(' ')[0] for column in columns_def), table_name) - columns_def = ['ADD COLUMN {}'.format(col_def) for col_def in columns_def] + columns_def = ['ADD {}'.format(col_def) for col_def in columns_def] try: engine.execute(text("ALTER TABLE {table} {columns_def}".format( @@ -147,7 +161,7 @@ def _add_columns(engine, table_name, columns_def): raise _LOGGER.warning('Column %s already exists on %s, continueing', - column_def.split(' ')[0], table_name) + column_def.split(' ')[1], table_name) def _apply_update(engine, new_version, old_version): From 1be388c5874a1880455a0a1d50935791c1ee72a3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 20 Aug 2018 11:52:23 +0200 Subject: [PATCH 35/37] Update frontend to 20180820.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 e17bbad78d1..a436cc483ae 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180818.0'] +REQUIREMENTS = ['home-assistant-frontend==20180820.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index a2b640cae1e..fe728bf2e02 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -432,7 +432,7 @@ hole==0.3.0 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180818.0 +home-assistant-frontend==20180820.0 # homeassistant.components.homekit_controller # homekit==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 24aa79587ad..7119259304e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.6 # homeassistant.components.frontend -home-assistant-frontend==20180818.0 +home-assistant-frontend==20180820.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From bd776c84bc2cd0cb31b86f99b1f3d17eb889fe1b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 21 Aug 2018 11:41:52 +0200 Subject: [PATCH 36/37] Forgiving add index in migration (#16092) --- homeassistant/components/recorder/migration.py | 11 ++++++++++- tests/components/recorder/test_migrate.py | 10 ++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 0dff21a5986..207f2f53a7f 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -57,6 +57,7 @@ def _create_index(engine, table_name, index_name): within the table definition described in the models """ from sqlalchemy import Table + from sqlalchemy.exc import OperationalError from . import models table = Table(table_name, models.Base.metadata) @@ -67,7 +68,15 @@ def _create_index(engine, table_name, index_name): _LOGGER.info("Adding index `%s` to database. Note: this can take several " "minutes on large databases and slow computers. Please " "be patient!", index_name) - index.create(engine) + try: + index.create(engine) + except OperationalError as err: + if 'already exists' not in str(err).lower(): + raise + + _LOGGER.warning('Index %s already exists on %s, continueing', + index_name, table_name) + _LOGGER.debug("Finished creating %s", index_name) diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 1c48c261372..93da4ec109b 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -80,3 +80,13 @@ def test_forgiving_add_column(): migration._add_columns(engine, 'hello', [ 'context_id CHARACTER(36)', ]) + + +def test_forgiving_add_index(): + """Test that add index will continue if index exists.""" + engine = create_engine( + 'sqlite://', + poolclass=StaticPool + ) + models.Base.metadata.create_all(engine) + migration._create_index(engine, "states", "ix_states_context_id") From 977d86e7ca88def52d5137ef7fc326f72ae5e9de Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 21 Aug 2018 11:43:14 +0200 Subject: [PATCH 37/37] Bumped version to 0.76.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 855f973554b..14054e44663 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 76 -PATCH_VERSION = '1' +PATCH_VERSION = '2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3)