From a269603e3b0fe1be899114a80ba9d3695e7a5552 Mon Sep 17 00:00:00 2001 From: dreed47 Date: Thu, 25 Apr 2019 00:11:07 -0400 Subject: [PATCH 01/14] fix for issue #21381 (#23306) --- homeassistant/components/zestimate/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zestimate/sensor.py b/homeassistant/components/zestimate/sensor.py index 0a1f14324f64..036422d6800f 100644 --- a/homeassistant/components/zestimate/sensor.py +++ b/homeassistant/components/zestimate/sensor.py @@ -47,10 +47,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Zestimate sensor.""" name = config.get(CONF_NAME) properties = config[CONF_ZPID] - params = {'zws-id': config[CONF_API_KEY]} sensors = [] for zpid in properties: + params = {'zws-id': config[CONF_API_KEY]} params['zpid'] = zpid sensors.append(ZestimateDataSensor(name, params)) add_entities(sensors, True) From 3bfb5b119a45d2dac12f9f070bfdd8994699ecd3 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Wed, 24 Apr 2019 14:47:22 -0700 Subject: [PATCH 02/14] Bump ecovacs lib 2 (#23354) * Bump Ecovacs dependency (sucks) Update to new version of sucks, which switches to a custom-built SleekXMPP that turns off certificate validation. This is to fix issues caused by Ecovacs serving invalid certificates. * Update requirements file --- homeassistant/components/ecovacs/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index d36768fb1b03..4495cb3c2f90 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -3,7 +3,7 @@ "name": "Ecovacs", "documentation": "https://www.home-assistant.io/components/ecovacs", "requirements": [ - "sucks==0.9.3" + "sucks==0.9.4" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 8772528c5205..8702cd1d4603 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1650,7 +1650,7 @@ steamodd==4.21 stringcase==1.2.0 # homeassistant.components.ecovacs -sucks==0.9.3 +sucks==0.9.4 # homeassistant.components.onvif suds-passworddigest-homeassistant==0.1.2a0.dev0 From dd1e352d1d43e4c5b79c506e4b1cdcfa7b671892 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Wed, 24 Apr 2019 23:31:32 -0500 Subject: [PATCH 03/14] Bump pyheos to 0.4.1 (#23360) * Bump pyheos==0.4.1 * Refresh player after reconnection --- homeassistant/components/heos/manifest.json | 2 +- homeassistant/components/heos/media_player.py | 10 +++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/heos/test_media_player.py | 31 +++++++++++++++++-- 5 files changed, 41 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/heos/manifest.json b/homeassistant/components/heos/manifest.json index 97b539356145..5b0a8e678938 100644 --- a/homeassistant/components/heos/manifest.json +++ b/homeassistant/components/heos/manifest.json @@ -3,7 +3,7 @@ "name": "Heos", "documentation": "https://www.home-assistant.io/components/heos", "requirements": [ - "pyheos==0.4.0" + "pyheos==0.4.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 8821591df207..e2a5b3177e08 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -1,4 +1,5 @@ """Denon HEOS Media Player.""" +import asyncio from functools import reduce, wraps import logging from operator import ior @@ -47,7 +48,7 @@ async def wrapper(*args, **kwargs): from pyheos import CommandError try: await func(*args, **kwargs) - except CommandError as ex: + except (CommandError, asyncio.TimeoutError, ConnectionError) as ex: _LOGGER.error("Unable to %s: %s", command, ex) return wrapper return decorator @@ -85,6 +86,13 @@ async def _controller_event(self, event): async def _heos_event(self, event): """Handle connection event.""" + from pyheos import CommandError, const + if event == const.EVENT_CONNECTED: + try: + await self._player.refresh() + except (CommandError, asyncio.TimeoutError, ConnectionError) as ex: + _LOGGER.error("Unable to refresh player %s: %s", + self._player, ex) await self.async_update_ha_state(True) async def _player_update(self, player_id, event): diff --git a/requirements_all.txt b/requirements_all.txt index 8702cd1d4603..b522da9f9c2e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1073,7 +1073,7 @@ pygtt==1.1.2 pyhaversion==2.2.1 # homeassistant.components.heos -pyheos==0.4.0 +pyheos==0.4.1 # homeassistant.components.hikvision pyhik==0.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ee0746f79550..2c0f5d40f4ad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -217,7 +217,7 @@ pydeconz==54 pydispatcher==2.0.5 # homeassistant.components.heos -pyheos==0.4.0 +pyheos==0.4.1 # homeassistant.components.homematic pyhomematic==0.1.58 diff --git a/tests/components/heos/test_media_player.py b/tests/components/heos/test_media_player.py index 0870f82b3ff0..4888018af044 100644 --- a/tests/components/heos/test_media_player.py +++ b/tests/components/heos/test_media_player.py @@ -108,13 +108,40 @@ async def test_updates_start_from_signals( state = hass.states.get('media_player.test_player') assert state.state == STATE_UNAVAILABLE - # Test heos events update + +async def test_updates_from_connection_event( + hass, config_entry, config, controller, input_sources, caplog): + """Tests player updates from connection event after connection failure.""" + # Connected + await setup_platform(hass, config_entry, config) + player = controller.players[1] player.available = True player.heos.dispatcher.send( const.SIGNAL_HEOS_EVENT, const.EVENT_CONNECTED) await hass.async_block_till_done() state = hass.states.get('media_player.test_player') - assert state.state == STATE_PLAYING + assert state.state == STATE_IDLE + assert player.refresh.call_count == 1 + + # Connected handles refresh failure + player.reset_mock() + player.refresh.side_effect = CommandError(None, "Failure", 1) + player.heos.dispatcher.send( + const.SIGNAL_HEOS_EVENT, const.EVENT_CONNECTED) + await hass.async_block_till_done() + state = hass.states.get('media_player.test_player') + assert player.refresh.call_count == 1 + assert "Unable to refresh player" in caplog.text + + # Disconnected + player.reset_mock() + player.available = False + player.heos.dispatcher.send( + const.SIGNAL_HEOS_EVENT, const.EVENT_DISCONNECTED) + await hass.async_block_till_done() + state = hass.states.get('media_player.test_player') + assert state.state == STATE_UNAVAILABLE + assert player.refresh.call_count == 0 async def test_updates_from_sources_updated( From ffcaeb4ef11d108da8d97db7bf669db28fe42b88 Mon Sep 17 00:00:00 2001 From: Chuang Zheng <545029543@qq.com> Date: Thu, 25 Apr 2019 20:50:28 +0800 Subject: [PATCH 04/14] async_setup_component stage_1_domains (#23375) --- homeassistant/bootstrap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index c2039161ceba..3959eb880351 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -355,7 +355,7 @@ async def _async_set_up_integrations( if stage_1_domains: await asyncio.gather(*[ async_setup_component(hass, domain, config) - for domain in logging_domains + for domain in stage_1_domains ]) # Load all integrations From 1ab03d9e151a5713f1589e6fb98f34fef03e1b85 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Thu, 25 Apr 2019 12:58:10 -0700 Subject: [PATCH 05/14] Add error handling for migration failure (#23383) --- homeassistant/config.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index a7267441cdb5..440082145351 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -398,8 +398,12 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None: if TTS_PRE_92 in config_raw: _LOGGER.info("Migrating google tts to google_translate tts") config_raw = config_raw.replace(TTS_PRE_92, TTS_92) - with open(config_path, 'wt', encoding='utf-8') as config_file: - config_file.write(config_raw) + try: + with open(config_path, 'wt', encoding='utf-8') as config_file: + config_file.write(config_raw) + except IOError: + _LOGGER.exception("Migrating to google_translate tts failed") + pass with open(version_path, 'wt') as outp: outp.write(__version__) From ed16681b8e83459d149f7a71a6c2eab66ca2fbee Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 26 Apr 2019 04:33:05 +0200 Subject: [PATCH 06/14] Broadlink fixup unintended breakage from service refactor (#23408) * Allow host/ipv6 address for broadlink service This matches switch config and is a regression fix * Restore padding of packets for broadlink * Drop unused import * Fix comment on test --- .../components/broadlink/__init__.py | 19 ++++++-------- tests/components/broadlink/test_init.py | 25 ++++++------------- 2 files changed, 15 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/broadlink/__init__.py b/homeassistant/components/broadlink/__init__.py index 3404bdef99b2..a1cc0a0caa3c 100644 --- a/homeassistant/components/broadlink/__init__.py +++ b/homeassistant/components/broadlink/__init__.py @@ -2,7 +2,6 @@ import asyncio from base64 import b64decode, b64encode import logging -import re import socket from datetime import timedelta @@ -19,26 +18,22 @@ DEFAULT_RETRY = 3 -def ipv4_address(value): - """Validate an ipv4 address.""" - regex = re.compile(r'^\d+\.\d+\.\d+\.\d+$') - if not regex.match(value): - raise vol.Invalid('Invalid Ipv4 address, expected a.b.c.d') - return value - - def data_packet(value): """Decode a data packet given for broadlink.""" - return b64decode(cv.string(value)) + value = cv.string(value) + extra = len(value) % 4 + if extra > 0: + value = value + ('=' * (4 - extra)) + return b64decode(value) SERVICE_SEND_SCHEMA = vol.Schema({ - vol.Required(CONF_HOST): ipv4_address, + vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PACKET): vol.All(cv.ensure_list, [data_packet]) }) SERVICE_LEARN_SCHEMA = vol.Schema({ - vol.Required(CONF_HOST): ipv4_address, + vol.Required(CONF_HOST): cv.string, }) diff --git a/tests/components/broadlink/test_init.py b/tests/components/broadlink/test_init.py index 5dca559cb0e5..44ae3d7612a6 100644 --- a/tests/components/broadlink/test_init.py +++ b/tests/components/broadlink/test_init.py @@ -4,10 +4,9 @@ from unittest.mock import MagicMock, patch, call import pytest -import voluptuous as vol from homeassistant.util.dt import utcnow -from homeassistant.components.broadlink import async_setup_service +from homeassistant.components.broadlink import async_setup_service, data_packet from homeassistant.components.broadlink.const import ( DOMAIN, SERVICE_LEARN, SERVICE_SEND) @@ -26,6 +25,13 @@ def dummy_broadlink(): yield broadlink +async def test_padding(hass): + """Verify that non padding strings are allowed.""" + assert data_packet('Jg') == b'&' + assert data_packet('Jg=') == b'&' + assert data_packet('Jg==') == b'&' + + async def test_send(hass): """Test send service.""" mock_device = MagicMock() @@ -100,18 +106,3 @@ async def test_learn_timeout(hass): assert mock_create.call_args == call( "No signal was received", title='Broadlink switch') - - -async def test_ipv4(): - """Test ipv4 parsing.""" - from homeassistant.components.broadlink import ipv4_address - - schema = vol.Schema(ipv4_address) - - for value in ('invalid', '1', '192', '192.168', - '192.168.0', '192.168.0.A'): - with pytest.raises(vol.MultipleInvalid): - schema(value) - - for value in ('192.168.0.1', '10.0.0.1'): - schema(value) From c1429f5d802b8e0ba356ab9f66caabd8890d14b0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 26 Apr 2019 12:41:30 -0700 Subject: [PATCH 07/14] Make setup more robust (#23414) * Make setup more robust * Fix typing --- homeassistant/setup.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 05e3307299a1..ee362ad130f5 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -151,9 +151,12 @@ def log_error(msg: str, link: bool = True) -> None: if hasattr(component, 'async_setup'): result = await component.async_setup( # type: ignore hass, processed_config) - else: + elif hasattr(component, 'setup'): result = await hass.async_add_executor_job( component.setup, hass, processed_config) # type: ignore + else: + log_error("No setup function defined.") + return False except Exception: # pylint: disable=broad-except _LOGGER.exception("Error during setup of component %s", domain) async_notify_setup_error(hass, domain, True) @@ -176,7 +179,7 @@ def log_error(msg: str, link: bool = True) -> None: for entry in hass.config_entries.async_entries(domain): await entry.async_setup(hass, integration=integration) - hass.config.components.add(component.DOMAIN) # type: ignore + hass.config.components.add(domain) # Cleanup if domain in hass.data[DATA_SETUP]: @@ -184,7 +187,7 @@ def log_error(msg: str, link: bool = True) -> None: hass.bus.async_fire( EVENT_COMPONENT_LOADED, - {ATTR_COMPONENT: component.DOMAIN} # type: ignore + {ATTR_COMPONENT: domain} ) return True From 46c955a5019c153981c4ec5f5620526230e438ce Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Fri, 26 Apr 2019 01:47:40 -0500 Subject: [PATCH 08/14] Add missing feature support flag (#23417) --- homeassistant/components/soundtouch/media_player.py | 9 +++++---- tests/components/soundtouch/test_media_player.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index a2a6c315edae..74c614c03a6c 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -5,11 +5,12 @@ import voluptuous as vol from homeassistant.components.media_player import ( - MediaPlayerDevice, PLATFORM_SCHEMA) + PLATFORM_SCHEMA, MediaPlayerDevice) from homeassistant.components.media_player.const import ( DOMAIN, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, - SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP) + SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_UNAVAILABLE) @@ -56,7 +57,7 @@ SUPPORT_SOUNDTOUCH = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \ SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF | \ - SUPPORT_VOLUME_SET | SUPPORT_TURN_ON | SUPPORT_PLAY + SUPPORT_VOLUME_SET | SUPPORT_TURN_ON | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, diff --git a/tests/components/soundtouch/test_media_player.py b/tests/components/soundtouch/test_media_player.py index 87f41b11d951..432229a482c6 100644 --- a/tests/components/soundtouch/test_media_player.py +++ b/tests/components/soundtouch/test_media_player.py @@ -372,7 +372,7 @@ def test_media_commands(self, mocked_soundtouch_device): mock.MagicMock()) assert mocked_soundtouch_device.call_count == 1 all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] - assert all_devices[0].supported_features == 17853 + assert all_devices[0].supported_features == 18365 @mock.patch('libsoundtouch.device.SoundTouchDevice.power_off') @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') From 1ec08ce243a9b3abdeb517a289d54638ca695cd1 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Thu, 25 Apr 2019 23:42:39 -0500 Subject: [PATCH 09/14] Fix supported features gates in media_player volume up/down services (#23419) * Correct media player feature gates * Fix failing test * Lint... --- .../components/media_player/__init__.py | 47 ++++++++++--------- homeassistant/helpers/service.py | 3 +- .../media_player/test_async_helpers.py | 14 ++++++ tests/helpers/test_service.py | 2 +- 4 files changed, 42 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 7dcfdac52179..6efbdd7c3d47 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -45,7 +45,8 @@ SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_STOP, SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET) + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP) from .reproduce_state import async_reproduce_states # noqa _LOGGER = logging.getLogger(__name__) @@ -164,77 +165,77 @@ async def async_setup(hass, config): component.async_register_entity_service( SERVICE_TURN_ON, MEDIA_PLAYER_SCHEMA, - 'async_turn_on', SUPPORT_TURN_ON + 'async_turn_on', [SUPPORT_TURN_ON] ) component.async_register_entity_service( SERVICE_TURN_OFF, MEDIA_PLAYER_SCHEMA, - 'async_turn_off', SUPPORT_TURN_OFF + 'async_turn_off', [SUPPORT_TURN_OFF] ) component.async_register_entity_service( SERVICE_TOGGLE, MEDIA_PLAYER_SCHEMA, - 'async_toggle', SUPPORT_TURN_OFF | SUPPORT_TURN_ON + 'async_toggle', [SUPPORT_TURN_OFF | SUPPORT_TURN_ON] ) component.async_register_entity_service( SERVICE_VOLUME_UP, MEDIA_PLAYER_SCHEMA, - 'async_volume_up', SUPPORT_VOLUME_SET + 'async_volume_up', [SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP] ) component.async_register_entity_service( SERVICE_VOLUME_DOWN, MEDIA_PLAYER_SCHEMA, - 'async_volume_down', SUPPORT_VOLUME_SET + 'async_volume_down', [SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP] ) component.async_register_entity_service( SERVICE_MEDIA_PLAY_PAUSE, MEDIA_PLAYER_SCHEMA, - 'async_media_play_pause', SUPPORT_PLAY | SUPPORT_PAUSE + 'async_media_play_pause', [SUPPORT_PLAY | SUPPORT_PAUSE] ) component.async_register_entity_service( SERVICE_MEDIA_PLAY, MEDIA_PLAYER_SCHEMA, - 'async_media_play', SUPPORT_PLAY + 'async_media_play', [SUPPORT_PLAY] ) component.async_register_entity_service( SERVICE_MEDIA_PAUSE, MEDIA_PLAYER_SCHEMA, - 'async_media_pause', SUPPORT_PAUSE + 'async_media_pause', [SUPPORT_PAUSE] ) component.async_register_entity_service( SERVICE_MEDIA_STOP, MEDIA_PLAYER_SCHEMA, - 'async_media_stop', SUPPORT_STOP + 'async_media_stop', [SUPPORT_STOP] ) component.async_register_entity_service( SERVICE_MEDIA_NEXT_TRACK, MEDIA_PLAYER_SCHEMA, - 'async_media_next_track', SUPPORT_NEXT_TRACK + 'async_media_next_track', [SUPPORT_NEXT_TRACK] ) component.async_register_entity_service( SERVICE_MEDIA_PREVIOUS_TRACK, MEDIA_PLAYER_SCHEMA, - 'async_media_previous_track', SUPPORT_PREVIOUS_TRACK + 'async_media_previous_track', [SUPPORT_PREVIOUS_TRACK] ) component.async_register_entity_service( SERVICE_CLEAR_PLAYLIST, MEDIA_PLAYER_SCHEMA, - 'async_clear_playlist', SUPPORT_CLEAR_PLAYLIST + 'async_clear_playlist', [SUPPORT_CLEAR_PLAYLIST] ) component.async_register_entity_service( SERVICE_VOLUME_SET, MEDIA_PLAYER_SET_VOLUME_SCHEMA, lambda entity, call: entity.async_set_volume_level( volume=call.data[ATTR_MEDIA_VOLUME_LEVEL]), - SUPPORT_VOLUME_SET + [SUPPORT_VOLUME_SET] ) component.async_register_entity_service( SERVICE_VOLUME_MUTE, MEDIA_PLAYER_MUTE_VOLUME_SCHEMA, lambda entity, call: entity.async_mute_volume( mute=call.data[ATTR_MEDIA_VOLUME_MUTED]), - SUPPORT_VOLUME_MUTE + [SUPPORT_VOLUME_MUTE] ) component.async_register_entity_service( SERVICE_MEDIA_SEEK, MEDIA_PLAYER_MEDIA_SEEK_SCHEMA, lambda entity, call: entity.async_media_seek( position=call.data[ATTR_MEDIA_SEEK_POSITION]), - SUPPORT_SEEK + [SUPPORT_SEEK] ) component.async_register_entity_service( SERVICE_SELECT_SOURCE, MEDIA_PLAYER_SELECT_SOURCE_SCHEMA, - 'async_select_source', SUPPORT_SELECT_SOURCE + 'async_select_source', [SUPPORT_SELECT_SOURCE] ) component.async_register_entity_service( SERVICE_SELECT_SOUND_MODE, MEDIA_PLAYER_SELECT_SOUND_MODE_SCHEMA, - 'async_select_sound_mode', SUPPORT_SELECT_SOUND_MODE + 'async_select_sound_mode', [SUPPORT_SELECT_SOUND_MODE] ) component.async_register_entity_service( SERVICE_PLAY_MEDIA, MEDIA_PLAYER_PLAY_MEDIA_SCHEMA, @@ -242,11 +243,11 @@ async def async_setup(hass, config): media_type=call.data[ATTR_MEDIA_CONTENT_TYPE], media_id=call.data[ATTR_MEDIA_CONTENT_ID], enqueue=call.data.get(ATTR_MEDIA_ENQUEUE) - ), SUPPORT_PLAY_MEDIA + ), [SUPPORT_PLAY_MEDIA] ) component.async_register_entity_service( SERVICE_SHUFFLE_SET, MEDIA_PLAYER_SET_SHUFFLE_SCHEMA, - 'async_set_shuffle', SUPPORT_SHUFFLE_SET + 'async_set_shuffle', [SUPPORT_SHUFFLE_SET] ) return True @@ -686,7 +687,8 @@ async def async_volume_up(self): await self.hass.async_add_job(self.volume_up) return - if self.volume_level < 1: + if self.volume_level < 1 \ + and self.supported_features & SUPPORT_VOLUME_SET: await self.async_set_volume_level(min(1, self.volume_level + .1)) async def async_volume_down(self): @@ -699,7 +701,8 @@ async def async_volume_down(self): await self.hass.async_add_job(self.volume_down) return - if self.volume_level > 0: + if self.volume_level > 0 \ + and self.supported_features & SUPPORT_VOLUME_SET: await self.async_set_volume_level( max(0, self.volume_level - .1)) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 8c576f58c14c..7eb72a66c8b0 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -327,7 +327,8 @@ async def _handle_service_platform_call(func, data, entities, context, # Skip entities that don't have the required feature. if required_features is not None \ - and not entity.supported_features & required_features: + and not any(entity.supported_features & feature_set + for feature_set in required_features): continue entity.async_set_context(context) diff --git a/tests/components/media_player/test_async_helpers.py b/tests/components/media_player/test_async_helpers.py index 1c4a2fa84a26..aa3d1eff2095 100644 --- a/tests/components/media_player/test_async_helpers.py +++ b/tests/components/media_player/test_async_helpers.py @@ -29,6 +29,13 @@ def volume_level(self): """Volume level of the media player (0..1).""" return self._volume + @property + def supported_features(self): + """Flag media player features that are supported.""" + return mp.const.SUPPORT_VOLUME_SET | mp.const.SUPPORT_PLAY \ + | mp.const.SUPPORT_PAUSE | mp.const.SUPPORT_TURN_OFF \ + | mp.const.SUPPORT_TURN_ON + @asyncio.coroutine def async_set_volume_level(self, volume): """Set volume level, range 0..1.""" @@ -74,6 +81,13 @@ def volume_level(self): """Volume level of the media player (0..1).""" return self._volume + @property + def supported_features(self): + """Flag media player features that are supported.""" + return mp.const.SUPPORT_VOLUME_SET | mp.const.SUPPORT_VOLUME_STEP \ + | mp.const.SUPPORT_PLAY | mp.const.SUPPORT_PAUSE \ + | mp.const.SUPPORT_TURN_OFF | mp.const.SUPPORT_TURN_ON + def set_volume_level(self, volume): """Set volume level, range 0..1.""" self._volume = volume diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 647ca981da3f..81cdd0978553 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -280,7 +280,7 @@ async def test_call_with_required_features(hass, mock_entities): Mock(entities=mock_entities) ], test_service_mock, ha.ServiceCall('test_domain', 'test_service', { 'entity_id': 'all' - }), required_features=1) + }), required_features=[1]) assert len(mock_entities) == 2 # Called once because only one of the entities had the required features assert test_service_mock.call_count == 1 From 065b077369d53b8ece732069538bad7efd8d59b2 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 26 Apr 2019 17:15:37 +0200 Subject: [PATCH 10/14] Refactor netatmo to use hass.data (#23429) * Refactor NETATMO_AUTH to use hass.data * Minor cleanup * Rename conf to auth and other suggestions by Martin * Revert webhook name change * Rename constant * Move auth * Don't use hass.data.get() * Fix auth string --- homeassistant/components/netatmo/__init__.py | 19 +++++++++++-------- .../components/netatmo/binary_sensor.py | 8 ++++++-- homeassistant/components/netatmo/camera.py | 8 ++++++-- homeassistant/components/netatmo/climate.py | 9 ++++++--- homeassistant/components/netatmo/const.py | 5 +++++ homeassistant/components/netatmo/sensor.py | 17 ++++++++++------- 6 files changed, 44 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/netatmo/const.py diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index cf64363ba503..9ed9051ed50c 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -12,6 +12,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle +from .const import DOMAIN, DATA_NETATMO_AUTH + _LOGGER = logging.getLogger(__name__) DATA_PERSONS = 'netatmo_persons' @@ -20,8 +22,6 @@ CONF_SECRET_KEY = 'secret_key' CONF_WEBHOOKS = 'webhooks' -DOMAIN = 'netatmo' - SERVICE_ADDWEBHOOK = 'addwebhook' SERVICE_DROPWEBHOOK = 'dropwebhook' @@ -83,10 +83,9 @@ def setup(hass, config): """Set up the Netatmo devices.""" import pyatmo - global NETATMO_AUTH hass.data[DATA_PERSONS] = {} try: - NETATMO_AUTH = pyatmo.ClientAuth( + auth = pyatmo.ClientAuth( config[DOMAIN][CONF_API_KEY], config[DOMAIN][CONF_SECRET_KEY], config[DOMAIN][CONF_USERNAME], config[DOMAIN][CONF_PASSWORD], 'read_station read_camera access_camera ' @@ -96,6 +95,9 @@ def setup(hass, config): _LOGGER.error("Unable to connect to Netatmo API") return False + # Store config to be used during entry setup + hass.data[DATA_NETATMO_AUTH] = auth + if config[DOMAIN][CONF_DISCOVERY]: for component in 'camera', 'sensor', 'binary_sensor', 'climate': discovery.load_platform(hass, component, DOMAIN, {}, config) @@ -107,7 +109,7 @@ def setup(hass, config): webhook_id) hass.components.webhook.async_register( DOMAIN, 'Netatmo', webhook_id, handle_webhook) - NETATMO_AUTH.addwebhook(hass.data[DATA_WEBHOOK_URL]) + auth.addwebhook(hass.data[DATA_WEBHOOK_URL]) hass.bus.listen_once( EVENT_HOMEASSISTANT_STOP, dropwebhook) @@ -117,7 +119,7 @@ def _service_addwebhook(service): if url is None: url = hass.data[DATA_WEBHOOK_URL] _LOGGER.info("Adding webhook for URL: %s", url) - NETATMO_AUTH.addwebhook(url) + auth.addwebhook(url) hass.services.register( DOMAIN, SERVICE_ADDWEBHOOK, _service_addwebhook, @@ -126,7 +128,7 @@ def _service_addwebhook(service): def _service_dropwebhook(service): """Service to drop webhooks during runtime.""" _LOGGER.info("Dropping webhook") - NETATMO_AUTH.dropwebhook() + auth.dropwebhook() hass.services.register( DOMAIN, SERVICE_DROPWEBHOOK, _service_dropwebhook, @@ -137,7 +139,8 @@ def _service_dropwebhook(service): def dropwebhook(hass): """Drop the webhook subscription.""" - NETATMO_AUTH.dropwebhook() + auth = hass.data[DATA_NETATMO_AUTH] + auth.dropwebhook() async def handle_webhook(hass, webhook_id, request): diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index f282faf82c87..432820d6dbd7 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -8,7 +8,8 @@ from homeassistant.const import CONF_TIMEOUT from homeassistant.helpers import config_validation as cv -from . import CameraData, NETATMO_AUTH +from .const import DATA_NETATMO_AUTH +from . import CameraData _LOGGER = logging.getLogger(__name__) @@ -59,8 +60,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): module_name = None import pyatmo + + auth = hass.data[DATA_NETATMO_AUTH] + try: - data = CameraData(hass, NETATMO_AUTH, home) + data = CameraData(hass, auth, home) if not data.get_camera_names(): return None except pyatmo.NoDevice: diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index b74dce4b2620..976e07949388 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -9,7 +9,8 @@ from homeassistant.const import CONF_VERIFY_SSL from homeassistant.helpers import config_validation as cv -from . import CameraData, NETATMO_AUTH +from .const import DATA_NETATMO_AUTH +from . import CameraData _LOGGER = logging.getLogger(__name__) @@ -37,8 +38,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): verify_ssl = config.get(CONF_VERIFY_SSL, True) quality = config.get(CONF_QUALITY, DEFAULT_QUALITY) import pyatmo + + auth = hass.data[DATA_NETATMO_AUTH] + try: - data = CameraData(hass, NETATMO_AUTH, home) + data = CameraData(hass, auth, home) for camera_name in data.get_camera_names(): camera_type = data.get_camera_type(camera=camera_name, home=home) if CONF_CAMERAS in config: diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 00c08c654ef0..33ad34b25ff3 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -14,7 +14,7 @@ STATE_OFF, TEMP_CELSIUS, ATTR_TEMPERATURE, CONF_NAME) from homeassistant.util import Throttle -from . import NETATMO_AUTH +from .const import DATA_NETATMO_AUTH _LOGGER = logging.getLogger(__name__) @@ -68,8 +68,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the NetAtmo Thermostat.""" import pyatmo homes_conf = config.get(CONF_HOMES) + + auth = hass.data[DATA_NETATMO_AUTH] + try: - home_data = HomeData(NETATMO_AUTH) + home_data = HomeData(auth) except pyatmo.NoDevice: return @@ -88,7 +91,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for home in homes: _LOGGER.debug("Setting up %s ...", home) try: - room_data = ThermostatData(NETATMO_AUTH, home) + room_data = ThermostatData(auth, home) except pyatmo.NoDevice: continue for room_id in room_data.get_room_ids(): diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py new file mode 100644 index 000000000000..ea547aaa52bb --- /dev/null +++ b/homeassistant/components/netatmo/const.py @@ -0,0 +1,5 @@ +"""Constants used by the Netatmo component.""" +DOMAIN = 'netatmo' + +DATA_NETATMO = 'netatmo' +DATA_NETATMO_AUTH = 'netatmo_auth' diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index c9c1101c2a2b..161177c9c763 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -12,7 +12,7 @@ from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -from . import NETATMO_AUTH +from .const import DATA_NETATMO_AUTH _LOGGER = logging.getLogger(__name__) @@ -68,23 +68,26 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the available Netatmo weather sensors.""" dev = [] + auth = hass.data[DATA_NETATMO_AUTH] + if CONF_MODULES in config: - manual_config(config, dev) + manual_config(auth, config, dev) else: - auto_config(config, dev) + auto_config(auth, config, dev) if dev: add_entities(dev, True) -def manual_config(config, dev): +def manual_config(auth, config, dev): """Handle manual configuration.""" import pyatmo all_classes = all_product_classes() not_handled = {} + for data_class in all_classes: - data = NetAtmoData(NETATMO_AUTH, data_class, + data = NetAtmoData(auth, data_class, config.get(CONF_STATION)) try: # Iterate each module @@ -107,12 +110,12 @@ def manual_config(config, dev): _LOGGER.error('Module name: "%s" not found', module_name) -def auto_config(config, dev): +def auto_config(auth, config, dev): """Handle auto configuration.""" import pyatmo for data_class in all_product_classes(): - data = NetAtmoData(NETATMO_AUTH, data_class, config.get(CONF_STATION)) + data = NetAtmoData(auth, data_class, config.get(CONF_STATION)) try: for module_name in data.get_module_names(): for variable in \ From f6a6be9a22b118fa32e99d5e26ef3fb103af014c Mon Sep 17 00:00:00 2001 From: Toon Willems Date: Fri, 26 Apr 2019 17:55:30 +0200 Subject: [PATCH 11/14] Fix Flux component (#23431) * Fix Flux component * Update manifest.json * Update manifest.json --- homeassistant/components/flux/manifest.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux/manifest.json b/homeassistant/components/flux/manifest.json index d4d67edbd353..9bf3ba09ce71 100644 --- a/homeassistant/components/flux/manifest.json +++ b/homeassistant/components/flux/manifest.json @@ -3,8 +3,7 @@ "name": "Flux", "documentation": "https://www.home-assistant.io/components/flux", "requirements": [], - "dependencies": [ - "light" - ], + "dependencies": [], + "after_dependencies": ["light"], "codeowners": [] } From 0d4858e29633641f279a11436360d8bcf3e26096 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Fri, 26 Apr 2019 20:24:02 +0200 Subject: [PATCH 12/14] Fix daikin setup (#23440) Fix daikin setup --- homeassistant/components/daikin/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index fc15ebea7727..edc447fe7214 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -63,10 +63,10 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): if not daikin_api: return False hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: daikin_api}) - await asyncio.wait([ - hass.config_entries.async_forward_entry_setup(entry, component) - for component in COMPONENT_TYPES - ]) + for component in COMPONENT_TYPES: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup( + entry, component)) return True From 95ed8fb24555fc7d19a2e1fa753c6b5e725f5a59 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Fri, 26 Apr 2019 20:56:55 +0200 Subject: [PATCH 13/14] Fix point setup (#23441) Fix point setup --- homeassistant/components/point/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index c0b2f7acd0fc..2ed83fe1d9b0 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -88,7 +88,7 @@ def token_saver(token): await async_setup_webhook(hass, entry, session) client = MinutPointClient(hass, entry, session) hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: client}) - await client.update() + hass.async_create_task(client.update()) return True From 081a0290ba02081d3327e964a04cc7534aec1756 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 26 Apr 2019 20:00:33 +0000 Subject: [PATCH 14/14] Bump version 0.91.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 9a376c04eeaf..f38b14969946 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 92 -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)