From f70a2ba1f71fa7dd13ee3216abcb5cc4ff2458cb Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 9 Apr 2020 17:49:09 -0500 Subject: [PATCH 01/18] Improve Plex debounce/throttle logic (#33805) * Improve Plex debounce/throttle logic * Use Debouncer helper, rewrite affected tests * Mock storage so files aren't left behind * Don't bother with wrapper method, store debouncer call during init * Test cleanup from review * Don't patch own code in tests --- homeassistant/components/plex/server.py | 38 ++--- tests/components/plex/common.py | 20 --- tests/components/plex/test_config_flow.py | 6 +- tests/components/plex/test_init.py | 155 +++++++++++--------- tests/components/plex/test_server.py | 166 +++++++++++++--------- 5 files changed, 200 insertions(+), 185 deletions(-) delete mode 100644 tests/components/plex/common.py diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 4134ad4e32b..d9e2d2bd9cc 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -1,5 +1,4 @@ """Shared class to maintain Plex server instances.""" -from functools import partial, wraps import logging import ssl from urllib.parse import urlparse @@ -13,8 +12,8 @@ import requests.exceptions from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import callback +from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.event import async_call_later from .const import ( CONF_CLIENT_IDENTIFIER, @@ -43,31 +42,6 @@ plexapi.X_PLEX_PRODUCT = X_PLEX_PRODUCT plexapi.X_PLEX_VERSION = X_PLEX_VERSION -def debounce(func): - """Decorate function to debounce callbacks from Plex websocket.""" - - unsub = None - - async def call_later_listener(self, _): - """Handle call_later callback.""" - nonlocal unsub - unsub = None - await func(self) - - @wraps(func) - async def wrapper(self): - """Schedule async callback.""" - nonlocal unsub - if unsub: - _LOGGER.debug("Throttling update of %s", self.friendly_name) - unsub() # pylint: disable=not-callable - unsub = async_call_later( - self.hass, DEBOUNCE_TIMEOUT, partial(call_later_listener, self), - ) - - return wrapper - - class PlexServer: """Manages a single Plex server connection.""" @@ -87,6 +61,13 @@ class PlexServer: self._accounts = [] self._owner_username = None self._version = None + self.async_update_platforms = Debouncer( + hass, + _LOGGER, + cooldown=DEBOUNCE_TIMEOUT, + immediate=True, + function=self._async_update_platforms, + ).async_call # Header conditionally added as it is not available in config entry v1 if CONF_CLIENT_IDENTIFIER in server_config: @@ -192,8 +173,7 @@ class PlexServer: """Fetch all data from the Plex server in a single method.""" return (self._plex_server.clients(), self._plex_server.sessions()) - @debounce - async def async_update_platforms(self): + async def _async_update_platforms(self): """Update the platform entities.""" _LOGGER.debug("Updating devices") diff --git a/tests/components/plex/common.py b/tests/components/plex/common.py deleted file mode 100644 index adc6f4e0299..00000000000 --- a/tests/components/plex/common.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Common fixtures and functions for Plex tests.""" -from datetime import timedelta - -from homeassistant.components.plex.const import ( - DEBOUNCE_TIMEOUT, - PLEX_UPDATE_PLATFORMS_SIGNAL, -) -from homeassistant.helpers.dispatcher import async_dispatcher_send -import homeassistant.util.dt as dt_util - -from tests.common import async_fire_time_changed - - -async def trigger_plex_update(hass, server_id): - """Update Plex by sending signal and jumping ahead by debounce timeout.""" - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() - next_update = dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT) - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index bd5d45c0246..d839ccc674b 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -15,13 +15,14 @@ from homeassistant.components.plex.const import ( CONF_USE_EPISODE_ART, DOMAIN, PLEX_SERVER_CONFIG, + PLEX_UPDATE_PLATFORMS_SIGNAL, SERVERS, ) from homeassistant.config_entries import ENTRY_STATE_LOADED from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN, CONF_URL +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component -from .common import trigger_plex_update from .const import DEFAULT_DATA, DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN from .mock_classes import MockPlexAccount, MockPlexServer @@ -415,7 +416,8 @@ async def test_option_flow_new_users_available(hass, caplog): server_id = mock_plex_server.machineIdentifier - await trigger_plex_update(hass, server_id) + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index cd1ea8725bd..ef2199b11c5 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -3,8 +3,9 @@ import copy from datetime import timedelta import ssl -from asynctest import patch +from asynctest import ClockedTestCase, patch import plexapi +import pytest import requests from homeassistant.components.media_player import DOMAIN as MP_DOMAIN @@ -23,14 +24,19 @@ from homeassistant.const import ( CONF_URL, CONF_VERIFY_SSL, ) +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from .common import trigger_plex_update from .const import DEFAULT_DATA, DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN from .mock_classes import MockPlexAccount, MockPlexServer -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + async_test_home_assistant, + mock_storage, +) async def test_setup_with_config(hass): @@ -67,70 +73,90 @@ async def test_setup_with_config(hass): assert loaded_server.plex_server == mock_plex_server - assert server_id in hass.data[const.DOMAIN][const.DISPATCHERS] - assert server_id in hass.data[const.DOMAIN][const.WEBSOCKETS] - assert ( - hass.data[const.DOMAIN][const.PLATFORMS_COMPLETED][server_id] == const.PLATFORMS - ) +class TestClockedPlex(ClockedTestCase): + """Create clock-controlled asynctest class.""" -async def test_setup_with_config_entry(hass, caplog): - """Test setup component with config.""" + @pytest.fixture(autouse=True) + def inject_fixture(self, caplog): + """Inject pytest fixtures as instance attributes.""" + self.caplog = caplog - mock_plex_server = MockPlexServer() + async def setUp(self): + """Initialize this test class.""" + self.hass = await async_test_home_assistant(self.loop) + self.mock_storage = mock_storage() + self.mock_storage.__enter__() - entry = MockConfigEntry( - domain=const.DOMAIN, - data=DEFAULT_DATA, - options=DEFAULT_OPTIONS, - unique_id=DEFAULT_DATA["server_id"], - ) + async def tearDown(self): + """Clean up the HomeAssistant instance.""" + await self.hass.async_stop() + self.mock_storage.__exit__(None, None, None) - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ) as mock_listen: - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) + async def test_setup_with_config_entry(self): + """Test setup component with config.""" + hass = self.hass + + mock_plex_server = MockPlexServer() + + entry = MockConfigEntry( + domain=const.DOMAIN, + data=DEFAULT_DATA, + options=DEFAULT_OPTIONS, + unique_id=DEFAULT_DATA["server_id"], + ) + + with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( + "homeassistant.components.plex.PlexWebsocket.listen" + ) as mock_listen: + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert mock_listen.called + + assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 + assert entry.state == ENTRY_STATE_LOADED + + server_id = mock_plex_server.machineIdentifier + loaded_server = hass.data[const.DOMAIN][const.SERVERS][server_id] + + assert loaded_server.plex_server == mock_plex_server + + async_dispatcher_send( + hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id) + ) await hass.async_block_till_done() - assert mock_listen.called + sensor = hass.states.get("sensor.plex_plex_server_1") + assert sensor.state == str(len(mock_plex_server.accounts)) - assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED - - server_id = mock_plex_server.machineIdentifier - loaded_server = hass.data[const.DOMAIN][const.SERVERS][server_id] - - assert loaded_server.plex_server == mock_plex_server - - assert server_id in hass.data[const.DOMAIN][const.DISPATCHERS] - assert server_id in hass.data[const.DOMAIN][const.WEBSOCKETS] - assert ( - hass.data[const.DOMAIN][const.PLATFORMS_COMPLETED][server_id] == const.PLATFORMS - ) - - await trigger_plex_update(hass, server_id) - - sensor = hass.states.get("sensor.plex_plex_server_1") - assert sensor.state == str(len(mock_plex_server.accounts)) - - await trigger_plex_update(hass, server_id) - - for test_exception in ( - plexapi.exceptions.BadRequest, - requests.exceptions.RequestException, - ): - with patch.object( - mock_plex_server, "clients", side_effect=test_exception - ) as patched_clients_bad_request: - await trigger_plex_update(hass, server_id) - - assert patched_clients_bad_request.called - assert ( - f"Could not connect to Plex server: {mock_plex_server.friendlyName}" - in caplog.text + # Ensure existing entities refresh + await self.advance(const.DEBOUNCE_TIMEOUT) + async_dispatcher_send( + hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id) ) - caplog.clear() + await hass.async_block_till_done() + + for test_exception in ( + plexapi.exceptions.BadRequest, + requests.exceptions.RequestException, + ): + with patch.object( + mock_plex_server, "clients", side_effect=test_exception + ) as patched_clients_bad_request: + await self.advance(const.DEBOUNCE_TIMEOUT) + async_dispatcher_send( + hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id) + ) + await hass.async_block_till_done() + + assert patched_clients_bad_request.called + assert ( + f"Could not connect to Plex server: {mock_plex_server.friendlyName}" + in self.caplog.text + ) + self.caplog.clear() async def test_set_config_entry_unique_id(hass): @@ -251,22 +277,12 @@ async def test_unload_config_entry(hass): assert loaded_server.plex_server == mock_plex_server - assert server_id in hass.data[const.DOMAIN][const.DISPATCHERS] - assert server_id in hass.data[const.DOMAIN][const.WEBSOCKETS] - assert ( - hass.data[const.DOMAIN][const.PLATFORMS_COMPLETED][server_id] == const.PLATFORMS - ) - with patch("homeassistant.components.plex.PlexWebsocket.close") as mock_close: await hass.config_entries.async_unload(entry.entry_id) assert mock_close.called assert entry.state == ENTRY_STATE_NOT_LOADED - assert server_id not in hass.data[const.DOMAIN][const.SERVERS] - assert server_id not in hass.data[const.DOMAIN][const.DISPATCHERS] - assert server_id not in hass.data[const.DOMAIN][const.WEBSOCKETS] - async def test_setup_with_photo_session(hass): """Test setup component with config.""" @@ -292,7 +308,8 @@ async def test_setup_with_photo_session(hass): server_id = mock_plex_server.machineIdentifier - await trigger_plex_update(hass, server_id) + async_dispatcher_send(hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() media_player = hass.states.get("media_player.plex_product_title") assert media_player.state == "idle" diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py index 3b70f30189a..6eff97ae7dc 100644 --- a/tests/components/plex/test_server.py +++ b/tests/components/plex/test_server.py @@ -1,8 +1,7 @@ """Tests for Plex server.""" import copy -from datetime import timedelta -from asynctest import patch +from asynctest import ClockedTestCase, patch from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.plex.const import ( @@ -14,13 +13,11 @@ from homeassistant.components.plex.const import ( SERVERS, ) from homeassistant.helpers.dispatcher import async_dispatcher_send -import homeassistant.util.dt as dt_util -from .common import trigger_plex_update from .const import DEFAULT_DATA, DEFAULT_OPTIONS from .mock_classes import MockPlexServer -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import MockConfigEntry, async_test_home_assistant, mock_storage async def test_new_users_available(hass): @@ -48,7 +45,8 @@ async def test_new_users_available(hass): server_id = mock_plex_server.machineIdentifier - await trigger_plex_update(hass, server_id) + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users @@ -86,7 +84,8 @@ async def test_new_ignored_users_available(hass, caplog): server_id = mock_plex_server.machineIdentifier - await trigger_plex_update(hass, server_id) + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users @@ -100,72 +99,109 @@ async def test_new_ignored_users_available(hass, caplog): assert sensor.state == str(len(mock_plex_server.accounts)) -async def test_mark_sessions_idle(hass): - """Test marking media_players as idle when sessions end.""" - entry = MockConfigEntry( - domain=DOMAIN, - data=DEFAULT_DATA, - options=DEFAULT_OPTIONS, - unique_id=DEFAULT_DATA["server_id"], - ) +class TestClockedPlex(ClockedTestCase): + """Create clock-controlled asynctest class.""" - mock_plex_server = MockPlexServer(config_entry=entry) + async def setUp(self): + """Initialize this test class.""" + self.hass = await async_test_home_assistant(self.loop) + self.mock_storage = mock_storage() + self.mock_storage.__enter__() - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) + async def tearDown(self): + """Clean up the HomeAssistant instance.""" + await self.hass.async_stop() + self.mock_storage.__exit__(None, None, None) + + async def test_mark_sessions_idle(self): + """Test marking media_players as idle when sessions end.""" + hass = self.hass + + entry = MockConfigEntry( + domain=DOMAIN, + data=DEFAULT_DATA, + options=DEFAULT_OPTIONS, + unique_id=DEFAULT_DATA["server_id"], + ) + + mock_plex_server = MockPlexServer(config_entry=entry) + + with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( + "homeassistant.components.plex.PlexWebsocket.listen" + ): + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + server_id = mock_plex_server.machineIdentifier + + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) await hass.async_block_till_done() - server_id = mock_plex_server.machineIdentifier + sensor = hass.states.get("sensor.plex_plex_server_1") + assert sensor.state == str(len(mock_plex_server.accounts)) - await trigger_plex_update(hass, server_id) + mock_plex_server.clear_clients() + mock_plex_server.clear_sessions() - sensor = hass.states.get("sensor.plex_plex_server_1") - assert sensor.state == str(len(mock_plex_server.accounts)) - - mock_plex_server.clear_clients() - mock_plex_server.clear_sessions() - - await trigger_plex_update(hass, server_id) - - sensor = hass.states.get("sensor.plex_plex_server_1") - assert sensor.state == "0" - - -async def test_debouncer(hass, caplog): - """Test debouncer decorator logic.""" - entry = MockConfigEntry( - domain=DOMAIN, - data=DEFAULT_DATA, - options=DEFAULT_OPTIONS, - unique_id=DEFAULT_DATA["server_id"], - ) - - mock_plex_server = MockPlexServer(config_entry=entry) - - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) + await self.advance(DEBOUNCE_TIMEOUT) + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) await hass.async_block_till_done() - server_id = mock_plex_server.machineIdentifier + sensor = hass.states.get("sensor.plex_plex_server_1") + assert sensor.state == "0" - # First two updates are skipped - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() + async def test_debouncer(self): + """Test debouncer behavior.""" + hass = self.hass - next_update = dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT) - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + entry = MockConfigEntry( + domain=DOMAIN, + data=DEFAULT_DATA, + options=DEFAULT_OPTIONS, + unique_id=DEFAULT_DATA["server_id"], + ) - assert ( - caplog.text.count(f"Throttling update of {mock_plex_server.friendlyName}") == 2 - ) + mock_plex_server = MockPlexServer(config_entry=entry) + + with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( + "homeassistant.components.plex.PlexWebsocket.listen" + ): + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + server_id = mock_plex_server.machineIdentifier + + with patch.object(mock_plex_server, "clients", return_value=[]), patch.object( + mock_plex_server, "sessions", return_value=[] + ) as mock_update: + # Called immediately + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() + assert mock_update.call_count == 1 + + # Throttled + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() + assert mock_update.call_count == 1 + + # Throttled + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() + assert mock_update.call_count == 1 + + # Called from scheduler + await self.advance(DEBOUNCE_TIMEOUT) + await hass.async_block_till_done() + assert mock_update.call_count == 2 + + # Throttled + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() + assert mock_update.call_count == 2 + + # Called from scheduler + await self.advance(DEBOUNCE_TIMEOUT) + await hass.async_block_till_done() + assert mock_update.call_count == 3 From 34fdf5a36f034c4429a599928f1574f36a49507c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Apr 2020 11:13:08 -0700 Subject: [PATCH 02/18] Update aioswitcher (#33821) --- homeassistant/components/switcher_kis/manifest.json | 3 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/switcher_kis/manifest.json b/homeassistant/components/switcher_kis/manifest.json index 81f5d2085c6..bc608276897 100644 --- a/homeassistant/components/switcher_kis/manifest.json +++ b/homeassistant/components/switcher_kis/manifest.json @@ -3,6 +3,5 @@ "name": "Switcher", "documentation": "https://www.home-assistant.io/integrations/switcher_kis/", "codeowners": ["@tomerfi"], - "requirements": ["aioswitcher==2019.4.26"], - "dependencies": [] + "requirements": ["aioswitcher==1.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index cd5dd932d4f..1d7dbe1135f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -208,7 +208,7 @@ aiopvpc==1.0.2 aiopylgtv==0.3.3 # homeassistant.components.switcher_kis -aioswitcher==2019.4.26 +aioswitcher==1.1.0 # homeassistant.components.unifi aiounifi==15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3e09e7916f5..3ad8676c9c8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -91,7 +91,7 @@ aiopvpc==1.0.2 aiopylgtv==0.3.3 # homeassistant.components.switcher_kis -aioswitcher==2019.4.26 +aioswitcher==1.1.0 # homeassistant.components.unifi aiounifi==15 From 9efbf2f880fd50dd4691ae1be0327afd043cf400 Mon Sep 17 00:00:00 2001 From: Lennart Henke Date: Thu, 9 Apr 2020 16:10:17 +0200 Subject: [PATCH 03/18] Fix nextcloud sensor mappings (#33840) --- homeassistant/components/nextcloud/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nextcloud/__init__.py b/homeassistant/components/nextcloud/__init__.py index 39eb16ec265..12c17e6081d 100644 --- a/homeassistant/components/nextcloud/__init__.py +++ b/homeassistant/components/nextcloud/__init__.py @@ -63,7 +63,7 @@ SENSORS = ( "nextcloud_storage_num_files", "nextcloud_storage_num_storages", "nextcloud_storage_num_storages_local", - "nextcloud_storage_num_storage_home", + "nextcloud_storage_num_storages_home", "nextcloud_storage_num_storages_other", "nextcloud_shares_num_shares", "nextcloud_shares_num_shares_user", @@ -83,9 +83,9 @@ SENSORS = ( "nextcloud_database_type", "nextcloud_database_version", "nextcloud_database_version", - "nextcloud_activeusers_last5minutes", - "nextcloud_activeusers_last1hour", - "nextcloud_activeusers_last24hours", + "nextcloud_activeUsers_last5minutes", + "nextcloud_activeUsers_last1hour", + "nextcloud_activeUsers_last24hours", ) From 995f5db9136a03bb8a4fab4244f8473e3484a454 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 9 Apr 2020 00:54:02 -0700 Subject: [PATCH 04/18] Check status code on onvif snapshot (#33865) --- homeassistant/components/onvif/camera.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 0c6a3bffa1b..f66f3a5c4aa 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -516,7 +516,8 @@ class ONVIFHassCamera(Camera): """Read image from a URL.""" try: response = requests.get(self._snapshot, timeout=5, auth=auth) - return response.content + if response.status_code < 300: + return response.content except requests.exceptions.RequestException as error: _LOGGER.error( "Fetch snapshot image failed from %s, falling back to FFmpeg; %s", From 2ff255dedcf85007830f77583e325b24dc3ab7c1 Mon Sep 17 00:00:00 2001 From: On Freund Date: Thu, 9 Apr 2020 18:04:12 +0300 Subject: [PATCH 05/18] Fix Monoprice robustness (#33869) * Silently handle update failures * Limite parallel updates * Remove return values * Remove trailing return * Add test for empty update --- .../components/monoprice/media_player.py | 15 ++++-- .../components/monoprice/test_media_player.py | 52 +++++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/monoprice/media_player.py b/homeassistant/components/monoprice/media_player.py index d85c219691e..9073ab224f1 100644 --- a/homeassistant/components/monoprice/media_player.py +++ b/homeassistant/components/monoprice/media_player.py @@ -1,6 +1,8 @@ """Support for interfacing with Monoprice 6 zone home audio controller.""" import logging +from serial import SerialException + from homeassistant import core from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( @@ -18,6 +20,8 @@ from .const import CONF_SOURCES, DOMAIN, SERVICE_RESTORE, SERVICE_SNAPSHOT _LOGGER = logging.getLogger(__name__) +PARALLEL_UPDATES = 1 + SUPPORT_MONOPRICE = ( SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET @@ -127,9 +131,15 @@ class MonopriceZone(MediaPlayerDevice): def update(self): """Retrieve latest state.""" - state = self._monoprice.zone_status(self._zone_id) + try: + state = self._monoprice.zone_status(self._zone_id) + except SerialException: + _LOGGER.warning("Could not update zone %d", self._zone_id) + return + if not state: - return False + return + self._state = STATE_ON if state.power else STATE_OFF self._volume = state.volume self._mute = state.mute @@ -138,7 +148,6 @@ class MonopriceZone(MediaPlayerDevice): self._source = self._source_id_name[idx] else: self._source = None - return True @property def entity_registry_enabled_default(self): diff --git a/tests/components/monoprice/test_media_player.py b/tests/components/monoprice/test_media_player.py index 3778f2af04b..bfe94023be2 100644 --- a/tests/components/monoprice/test_media_player.py +++ b/tests/components/monoprice/test_media_player.py @@ -294,6 +294,58 @@ async def test_update(hass): assert "three" == state.attributes[ATTR_INPUT_SOURCE] +async def test_failed_update(hass): + """Test updating failure from monoprice.""" + monoprice = MockMonoprice() + await _setup_monoprice(hass, monoprice) + + # Changing media player to new state + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0} + ) + await _call_media_player_service( + hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "one"} + ) + + monoprice.set_source(11, 3) + monoprice.set_volume(11, 38) + + with patch.object(MockMonoprice, "zone_status", side_effect=SerialException): + await async_update_entity(hass, ZONE_1_ID) + await hass.async_block_till_done() + + state = hass.states.get(ZONE_1_ID) + + assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.0 + assert state.attributes[ATTR_INPUT_SOURCE] == "one" + + +async def test_empty_update(hass): + """Test updating with no state from monoprice.""" + monoprice = MockMonoprice() + await _setup_monoprice(hass, monoprice) + + # Changing media player to new state + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0} + ) + await _call_media_player_service( + hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "one"} + ) + + monoprice.set_source(11, 3) + monoprice.set_volume(11, 38) + + with patch.object(MockMonoprice, "zone_status", return_value=None): + await async_update_entity(hass, ZONE_1_ID) + await hass.async_block_till_done() + + state = hass.states.get(ZONE_1_ID) + + assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.0 + assert state.attributes[ATTR_INPUT_SOURCE] == "one" + + async def test_supported_features(hass): """Test supported features property.""" await _setup_monoprice(hass, MockMonoprice()) From b2083a7bee9fd4f693cd30752ddc8c4fecbde268 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 9 Apr 2020 13:53:23 +0200 Subject: [PATCH 06/18] Fix modbus default delay (#33877) * solve modbus issue #33872 CONF_DELAY was used in a serial connection, which is not permitted. Sometimes async_update is called after async_setup is completed, but before event EVENT_HOMEASSISTANT_START is issued, leading to a missing object. * resolve review comment. Do not wait for start event, but activate pymodbus directly in async setup. * review 2 Remark, this does not work, async_setup hangs. clean start_modbus() from async calls, leaving only the pymodbus setup. * review 2a Moved listen_once back to start_modbus, since it is sync. --- homeassistant/components/modbus/__init__.py | 33 ++++++++++----------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index eb0e1b30d8a..14584ea17a0 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -21,7 +21,6 @@ from homeassistant.const import ( CONF_PORT, CONF_TIMEOUT, CONF_TYPE, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ) import homeassistant.helpers.config_validation as cv @@ -107,7 +106,7 @@ async def async_setup(hass, config): for client in hub_collect.values(): del client - def start_modbus(event): + def start_modbus(): """Start Modbus service.""" for client in hub_collect.values(): _LOGGER.debug("setup hub %s", client.name) @@ -115,20 +114,6 @@ async def async_setup(hass, config): hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus) - # Register services for modbus - hass.services.async_register( - MODBUS_DOMAIN, - SERVICE_WRITE_REGISTER, - write_register, - schema=SERVICE_WRITE_REGISTER_SCHEMA, - ) - hass.services.async_register( - MODBUS_DOMAIN, - SERVICE_WRITE_COIL, - write_coil, - schema=SERVICE_WRITE_COIL_SCHEMA, - ) - async def write_register(service): """Write Modbus registers.""" unit = int(float(service.data[ATTR_UNIT])) @@ -152,8 +137,19 @@ async def async_setup(hass, config): client_name = service.data[ATTR_HUB] await hub_collect[client_name].write_coil(unit, address, state) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_modbus) + # do not wait for EVENT_HOMEASSISTANT_START, activate pymodbus now + await hass.async_add_executor_job(start_modbus) + # Register services for modbus + hass.services.async_register( + MODBUS_DOMAIN, + SERVICE_WRITE_REGISTER, + write_register, + schema=SERVICE_WRITE_REGISTER_SCHEMA, + ) + hass.services.async_register( + MODBUS_DOMAIN, SERVICE_WRITE_COIL, write_coil, schema=SERVICE_WRITE_COIL_SCHEMA, + ) return True @@ -172,7 +168,7 @@ class ModbusHub: self._config_type = client_config[CONF_TYPE] self._config_port = client_config[CONF_PORT] self._config_timeout = client_config[CONF_TIMEOUT] - self._config_delay = client_config[CONF_DELAY] + self._config_delay = 0 if self._config_type == "serial": # serial configuration @@ -184,6 +180,7 @@ class ModbusHub: else: # network configuration self._config_host = client_config[CONF_HOST] + self._config_delay = client_config[CONF_DELAY] @property def name(self): From ecb37d0bdf3a95195739e064b56a8094002cb782 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 9 Apr 2020 17:21:01 +0200 Subject: [PATCH 07/18] Updated frontend to 20200407.2 (#33891) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index efd9f99b18a..3a4919dacae 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20200407.1"], + "requirements": ["home-assistant-frontend==20200407.2"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bf6888e7073..92564cd6781 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.32.2 -home-assistant-frontend==20200407.1 +home-assistant-frontend==20200407.2 importlib-metadata==1.5.0 jinja2>=2.11.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1d7dbe1135f..a923602b107 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -704,7 +704,7 @@ hole==0.5.1 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200407.1 +home-assistant-frontend==20200407.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3ad8676c9c8..c8368b00a80 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -282,7 +282,7 @@ hole==0.5.1 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200407.1 +home-assistant-frontend==20200407.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From c3ac8869b084f12dc82fdd7ba15e4b9f4125bfcd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 9 Apr 2020 09:51:23 -0700 Subject: [PATCH 08/18] Fix onvif consistent return (#33898) --- homeassistant/components/onvif/camera.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index f66f3a5c4aa..a0bfbab9b4f 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -525,6 +525,8 @@ class ONVIFHassCamera(Camera): error, ) + return None + image = await self.hass.async_add_job(fetch) if image is None: From 64bdf2d35b2b9276eb9c8f2a64d8b8510270a5c6 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 6 Apr 2020 01:09:20 +0200 Subject: [PATCH 09/18] Modbus: isolate common test functions (#33447) Since all entity test functions are going to use the modbus class, isolate the common parts in conftest.py, and thereby make it simpler to write additional test cases. cleaned up test_modbus_sensor.py while splitting the code. --- tests/components/modbus/conftest.py | 96 +++++++++++ tests/components/modbus/test_modbus_sensor.py | 157 +++++++++--------- 2 files changed, 177 insertions(+), 76 deletions(-) create mode 100644 tests/components/modbus/conftest.py diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py new file mode 100644 index 00000000000..043236c503c --- /dev/null +++ b/tests/components/modbus/conftest.py @@ -0,0 +1,96 @@ +"""The tests for the Modbus sensor component.""" +from datetime import timedelta +import logging +from unittest import mock + +import pytest + +from homeassistant.components.modbus.const import ( + CALL_TYPE_REGISTER_INPUT, + CONF_REGISTER, + CONF_REGISTER_TYPE, + CONF_REGISTERS, + DEFAULT_HUB, + MODBUS_DOMAIN, +) +from homeassistant.const import CONF_NAME, CONF_PLATFORM, CONF_SCAN_INTERVAL +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from tests.common import MockModule, async_fire_time_changed, mock_integration + +_LOGGER = logging.getLogger(__name__) + + +@pytest.fixture() +def mock_hub(hass): + """Mock hub.""" + mock_integration(hass, MockModule(MODBUS_DOMAIN)) + hub = mock.MagicMock() + hub.name = "hub" + hass.data[MODBUS_DOMAIN] = {DEFAULT_HUB: hub} + return hub + + +common_register_config = {CONF_NAME: "test-config", CONF_REGISTER: 1234} + + +class ReadResult: + """Storage class for register read results.""" + + def __init__(self, register_words): + """Init.""" + self.registers = register_words + + +read_result = None + + +async def simulate_read_registers(unit, address, count): + """Simulate modbus register read.""" + del unit, address, count # not used in simulation, but in real connection + global read_result + return read_result + + +async def run_test( + hass, mock_hub, register_config, entity_domain, register_words, expected +): + """Run test for given config and check that sensor outputs expected result.""" + + # Full sensor configuration + sensor_name = "modbus_test_sensor" + scan_interval = 5 + config = { + entity_domain: { + CONF_PLATFORM: "modbus", + CONF_SCAN_INTERVAL: scan_interval, + CONF_REGISTERS: [ + dict(**{CONF_NAME: sensor_name, CONF_REGISTER: 1234}, **register_config) + ], + } + } + + # Setup inputs for the sensor + global read_result + read_result = ReadResult(register_words) + if register_config.get(CONF_REGISTER_TYPE) == CALL_TYPE_REGISTER_INPUT: + mock_hub.read_input_registers = simulate_read_registers + else: + mock_hub.read_holding_registers = simulate_read_registers + + # Initialize sensor + now = dt_util.utcnow() + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + assert await async_setup_component(hass, entity_domain, config) + + # Trigger update call with time_changed event + now += timedelta(seconds=scan_interval + 1) + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + + # Check state + entity_id = f"{entity_domain}.{sensor_name}" + state = hass.states.get(entity_id).state + assert state == expected diff --git a/tests/components/modbus/test_modbus_sensor.py b/tests/components/modbus/test_modbus_sensor.py index 1c4094387a9..6207a363937 100644 --- a/tests/components/modbus/test_modbus_sensor.py +++ b/tests/components/modbus/test_modbus_sensor.py @@ -1,8 +1,5 @@ """The tests for the Modbus sensor component.""" -from datetime import timedelta -from unittest import mock - -import pytest +import logging from homeassistant.components.modbus.const import ( CALL_TYPE_REGISTER_HOLDING, @@ -11,78 +8,18 @@ from homeassistant.components.modbus.const import ( CONF_DATA_TYPE, CONF_OFFSET, CONF_PRECISION, - CONF_REGISTER, CONF_REGISTER_TYPE, - CONF_REGISTERS, CONF_REVERSE_ORDER, CONF_SCALE, DATA_TYPE_FLOAT, DATA_TYPE_INT, DATA_TYPE_UINT, - DEFAULT_HUB, - MODBUS_DOMAIN, ) -from homeassistant.const import CONF_NAME, CONF_PLATFORM, CONF_SCAN_INTERVAL -from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from tests.common import MockModule, async_fire_time_changed, mock_integration +from .conftest import run_test - -@pytest.fixture() -def mock_hub(hass): - """Mock hub.""" - mock_integration(hass, MockModule(MODBUS_DOMAIN)) - hub = mock.MagicMock() - hub.name = "hub" - hass.data[MODBUS_DOMAIN] = {DEFAULT_HUB: hub} - return hub - - -common_register_config = {CONF_NAME: "test-config", CONF_REGISTER: 1234} - - -class ReadResult: - """Storage class for register read results.""" - - def __init__(self, register_words): - """Init.""" - self.registers = register_words - - -async def run_test(hass, mock_hub, register_config, register_words, expected): - """Run test for given config and check that sensor outputs expected result.""" - - # Full sensor configuration - sensor_name = "modbus_test_sensor" - scan_interval = 5 - config = { - MODBUS_DOMAIN: { - CONF_PLATFORM: "modbus", - CONF_SCAN_INTERVAL: scan_interval, - CONF_REGISTERS: [ - dict(**{CONF_NAME: sensor_name, CONF_REGISTER: 1234}, **register_config) - ], - } - } - - # Setup inputs for the sensor - read_result = ReadResult(register_words) - if register_config.get(CONF_REGISTER_TYPE) == CALL_TYPE_REGISTER_INPUT: - mock_hub.read_input_registers.return_value = read_result - else: - mock_hub.read_holding_registers.return_value = read_result - - # Initialize sensor - now = dt_util.utcnow() - with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): - assert await async_setup_component(hass, MODBUS_DOMAIN, config) - - # Trigger update call with time_changed event - now += timedelta(seconds=scan_interval + 1) - with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): - async_fire_time_changed(hass, now) - await hass.async_block_till_done() +_LOGGER = logging.getLogger(__name__) async def test_simple_word_register(hass, mock_hub): @@ -94,14 +31,26 @@ async def test_simple_word_register(hass, mock_hub): CONF_OFFSET: 0, CONF_PRECISION: 0, } - await run_test(hass, mock_hub, register_config, register_words=[0], expected="0") + await run_test( + hass, + mock_hub, + register_config, + SENSOR_DOMAIN, + register_words=[0], + expected="0", + ) async def test_optional_conf_keys(hass, mock_hub): """Test handling of optional configuration keys.""" register_config = {} await run_test( - hass, mock_hub, register_config, register_words=[0x8000], expected="-32768" + hass, + mock_hub, + register_config, + SENSOR_DOMAIN, + register_words=[0x8000], + expected="-32768", ) @@ -114,7 +63,14 @@ async def test_offset(hass, mock_hub): CONF_OFFSET: 13, CONF_PRECISION: 0, } - await run_test(hass, mock_hub, register_config, register_words=[7], expected="20") + await run_test( + hass, + mock_hub, + register_config, + SENSOR_DOMAIN, + register_words=[7], + expected="20", + ) async def test_scale_and_offset(hass, mock_hub): @@ -126,7 +82,14 @@ async def test_scale_and_offset(hass, mock_hub): CONF_OFFSET: 13, CONF_PRECISION: 0, } - await run_test(hass, mock_hub, register_config, register_words=[7], expected="34") + await run_test( + hass, + mock_hub, + register_config, + SENSOR_DOMAIN, + register_words=[7], + expected="34", + ) async def test_ints_can_have_precision(hass, mock_hub): @@ -139,7 +102,12 @@ async def test_ints_can_have_precision(hass, mock_hub): CONF_PRECISION: 4, } await run_test( - hass, mock_hub, register_config, register_words=[7], expected="34.0000" + hass, + mock_hub, + register_config, + SENSOR_DOMAIN, + register_words=[7], + expected="34.0000", ) @@ -152,7 +120,14 @@ async def test_floats_get_rounded_correctly(hass, mock_hub): CONF_OFFSET: 0, CONF_PRECISION: 0, } - await run_test(hass, mock_hub, register_config, register_words=[1], expected="2") + await run_test( + hass, + mock_hub, + register_config, + SENSOR_DOMAIN, + register_words=[1], + expected="2", + ) async def test_parameters_as_strings(hass, mock_hub): @@ -164,7 +139,14 @@ async def test_parameters_as_strings(hass, mock_hub): CONF_OFFSET: "5", CONF_PRECISION: "1", } - await run_test(hass, mock_hub, register_config, register_words=[9], expected="18.5") + await run_test( + hass, + mock_hub, + register_config, + SENSOR_DOMAIN, + register_words=[9], + expected="18.5", + ) async def test_floating_point_scale(hass, mock_hub): @@ -176,7 +158,14 @@ async def test_floating_point_scale(hass, mock_hub): CONF_OFFSET: 0, CONF_PRECISION: 2, } - await run_test(hass, mock_hub, register_config, register_words=[1], expected="2.40") + await run_test( + hass, + mock_hub, + register_config, + SENSOR_DOMAIN, + register_words=[1], + expected="2.40", + ) async def test_floating_point_offset(hass, mock_hub): @@ -188,7 +177,14 @@ async def test_floating_point_offset(hass, mock_hub): CONF_OFFSET: -10.3, CONF_PRECISION: 1, } - await run_test(hass, mock_hub, register_config, register_words=[2], expected="-8.3") + await run_test( + hass, + mock_hub, + register_config, + SENSOR_DOMAIN, + register_words=[2], + expected="-8.3", + ) async def test_signed_two_word_register(hass, mock_hub): @@ -204,6 +200,7 @@ async def test_signed_two_word_register(hass, mock_hub): hass, mock_hub, register_config, + SENSOR_DOMAIN, register_words=[0x89AB, 0xCDEF], expected="-1985229329", ) @@ -222,6 +219,7 @@ async def test_unsigned_two_word_register(hass, mock_hub): hass, mock_hub, register_config, + SENSOR_DOMAIN, register_words=[0x89AB, 0xCDEF], expected=str(0x89ABCDEF), ) @@ -238,6 +236,7 @@ async def test_reversed(hass, mock_hub): hass, mock_hub, register_config, + SENSOR_DOMAIN, register_words=[0x89AB, 0xCDEF], expected=str(0xCDEF89AB), ) @@ -256,6 +255,7 @@ async def test_four_word_register(hass, mock_hub): hass, mock_hub, register_config, + SENSOR_DOMAIN, register_words=[0x89AB, 0xCDEF, 0x0123, 0x4567], expected="9920249030613615975", ) @@ -274,6 +274,7 @@ async def test_four_word_register_precision_is_intact_with_int_params(hass, mock hass, mock_hub, register_config, + SENSOR_DOMAIN, register_words=[0x0123, 0x4567, 0x89AB, 0xCDEF], expected="163971058432973793", ) @@ -292,6 +293,7 @@ async def test_four_word_register_precision_is_lost_with_float_params(hass, mock hass, mock_hub, register_config, + SENSOR_DOMAIN, register_words=[0x0123, 0x4567, 0x89AB, 0xCDEF], expected="163971058432973792", ) @@ -311,6 +313,7 @@ async def test_two_word_input_register(hass, mock_hub): hass, mock_hub, register_config, + SENSOR_DOMAIN, register_words=[0x89AB, 0xCDEF], expected=str(0x89ABCDEF), ) @@ -330,6 +333,7 @@ async def test_two_word_holding_register(hass, mock_hub): hass, mock_hub, register_config, + SENSOR_DOMAIN, register_words=[0x89AB, 0xCDEF], expected=str(0x89ABCDEF), ) @@ -349,6 +353,7 @@ async def test_float_data_type(hass, mock_hub): hass, mock_hub, register_config, + SENSOR_DOMAIN, register_words=[16286, 1617], expected="1.23457", ) From 27134696518ff196732d67730efc04ba5912d36f Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 7 Apr 2020 16:56:48 +0200 Subject: [PATCH 10/18] Fix Modbus review comments (#33755) * update common test for modbus integration * remove log messages from modbus setup function. * Make global method local * Change parameter name to snake_case --- homeassistant/components/modbus/__init__.py | 4 ---- .../components/modbus/binary_sensor.py | 4 ++-- homeassistant/components/modbus/climate.py | 4 ++-- homeassistant/components/modbus/sensor.py | 4 ++-- homeassistant/components/modbus/switch.py | 4 ++-- tests/components/modbus/conftest.py | 22 +++++++------------ 6 files changed, 16 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 14584ea17a0..3c488bd3245 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -97,7 +97,6 @@ async def async_setup(hass, config): """Set up Modbus component.""" hass.data[MODBUS_DOMAIN] = hub_collect = {} - _LOGGER.debug("registering hubs") for client_config in config[MODBUS_DOMAIN]: hub_collect[client_config[CONF_NAME]] = ModbusHub(client_config, hass.loop) @@ -109,7 +108,6 @@ async def async_setup(hass, config): def start_modbus(): """Start Modbus service.""" for client in hub_collect.values(): - _LOGGER.debug("setup hub %s", client.name) client.setup() hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus) @@ -158,7 +156,6 @@ class ModbusHub: def __init__(self, client_config, main_loop): """Initialize the Modbus hub.""" - _LOGGER.debug("Preparing setup: %s", client_config) # generic configuration self._loop = main_loop @@ -198,7 +195,6 @@ class ModbusHub: # Client* do deliver loop, client as result but # pylint does not accept that fact - _LOGGER.debug("doing setup") if self._config_type == "serial": _, self._client = ClientSerial( schedulers.ASYNC_IO, diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 51dfb7c5795..9989b9d530a 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -54,7 +54,7 @@ PLATFORM_SCHEMA = vol.All( ) -async def async_setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Modbus binary sensors.""" sensors = [] for entry in config[CONF_INPUTS]: @@ -70,7 +70,7 @@ async def async_setup_platform(hass, config, add_entities, discovery_info=None): ) ) - add_entities(sensors) + async_add_entities(sensors) class ModbusBinarySensor(BinarySensorDevice): diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 182dfeef2de..e5fbcf4d421 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -72,7 +72,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Modbus Thermostat Platform.""" name = config[CONF_NAME] modbus_slave = config[CONF_SLAVE] @@ -91,7 +91,7 @@ async def async_setup_platform(hass, config, add_entities, discovery_info=None): hub_name = config[CONF_HUB] hub = hass.data[MODBUS_DOMAIN][hub_name] - add_entities( + async_add_entities( [ ModbusThermostat( hub, diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 8c2b950648b..988d495eba5 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -89,7 +89,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Modbus sensors.""" sensors = [] data_types = {DATA_TYPE_INT: {1: "h", 2: "i", 4: "q"}} @@ -148,7 +148,7 @@ async def async_setup_platform(hass, config, add_entities, discovery_info=None): if not sensors: return False - add_entities(sensors) + async_add_entities(sensors) class ModbusRegisterSensor(RestoreEntity): diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index d7d6f121874..e4ec6a004fb 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -76,7 +76,7 @@ PLATFORM_SCHEMA = vol.All( ) -async def async_setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Read configuration and create Modbus devices.""" switches = [] if CONF_COILS in config: @@ -109,7 +109,7 @@ async def async_setup_platform(hass, config, add_entities, discovery_info=None): ) ) - add_entities(switches) + async_add_entities(switches) class ModbusCoilSwitch(ToggleEntity, RestoreEntity): diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index 043236c503c..d2fff820cdb 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -32,9 +32,6 @@ def mock_hub(hass): return hub -common_register_config = {CONF_NAME: "test-config", CONF_REGISTER: 1234} - - class ReadResult: """Storage class for register read results.""" @@ -46,18 +43,16 @@ class ReadResult: read_result = None -async def simulate_read_registers(unit, address, count): - """Simulate modbus register read.""" - del unit, address, count # not used in simulation, but in real connection - global read_result - return read_result - - async def run_test( - hass, mock_hub, register_config, entity_domain, register_words, expected + hass, use_mock_hub, register_config, entity_domain, register_words, expected ): """Run test for given config and check that sensor outputs expected result.""" + async def simulate_read_registers(unit, address, count): + """Simulate modbus register read.""" + del unit, address, count # not used in simulation, but in real connection + return read_result + # Full sensor configuration sensor_name = "modbus_test_sensor" scan_interval = 5 @@ -72,12 +67,11 @@ async def run_test( } # Setup inputs for the sensor - global read_result read_result = ReadResult(register_words) if register_config.get(CONF_REGISTER_TYPE) == CALL_TYPE_REGISTER_INPUT: - mock_hub.read_input_registers = simulate_read_registers + use_mock_hub.read_input_registers = simulate_read_registers else: - mock_hub.read_holding_registers = simulate_read_registers + use_mock_hub.read_holding_registers = simulate_read_registers # Initialize sensor now = dt_util.utcnow() From 54bf83855ca6d0c4097a79edae978c610c1017d0 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 9 Apr 2020 22:15:20 +0200 Subject: [PATCH 11/18] Rename domain import in modbus (#33906) --- homeassistant/components/modbus/__init__.py | 13 ++++++------- tests/components/modbus/conftest.py | 6 +++--- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 3c488bd3245..1e889043fae 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -35,7 +35,7 @@ from .const import ( CONF_PARITY, CONF_STOPBITS, DEFAULT_HUB, - MODBUS_DOMAIN, + MODBUS_DOMAIN as DOMAIN, SERVICE_WRITE_COIL, SERVICE_WRITE_REGISTER, ) @@ -68,7 +68,7 @@ ETHERNET_SCHEMA = BASE_SCHEMA.extend( ) CONFIG_SCHEMA = vol.Schema( - {MODBUS_DOMAIN: vol.All(cv.ensure_list, [vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA)])}, + {DOMAIN: vol.All(cv.ensure_list, [vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA)])}, extra=vol.ALLOW_EXTRA, ) @@ -95,9 +95,9 @@ SERVICE_WRITE_COIL_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up Modbus component.""" - hass.data[MODBUS_DOMAIN] = hub_collect = {} + hass.data[DOMAIN] = hub_collect = {} - for client_config in config[MODBUS_DOMAIN]: + for client_config in config[DOMAIN]: hub_collect[client_config[CONF_NAME]] = ModbusHub(client_config, hass.loop) def stop_modbus(event): @@ -140,13 +140,13 @@ async def async_setup(hass, config): # Register services for modbus hass.services.async_register( - MODBUS_DOMAIN, + DOMAIN, SERVICE_WRITE_REGISTER, write_register, schema=SERVICE_WRITE_REGISTER_SCHEMA, ) hass.services.async_register( - MODBUS_DOMAIN, SERVICE_WRITE_COIL, write_coil, schema=SERVICE_WRITE_COIL_SCHEMA, + DOMAIN, SERVICE_WRITE_COIL, write_coil, schema=SERVICE_WRITE_COIL_SCHEMA, ) return True @@ -204,7 +204,6 @@ class ModbusHub: stopbits=self._config_stopbits, bytesize=self._config_bytesize, parity=self._config_parity, - timeout=self._config_timeout, loop=self._loop, ) elif self._config_type == "rtuovertcp": diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index d2fff820cdb..d9cd62313b4 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -11,7 +11,7 @@ from homeassistant.components.modbus.const import ( CONF_REGISTER_TYPE, CONF_REGISTERS, DEFAULT_HUB, - MODBUS_DOMAIN, + MODBUS_DOMAIN as DOMAIN, ) from homeassistant.const import CONF_NAME, CONF_PLATFORM, CONF_SCAN_INTERVAL from homeassistant.setup import async_setup_component @@ -25,10 +25,10 @@ _LOGGER = logging.getLogger(__name__) @pytest.fixture() def mock_hub(hass): """Mock hub.""" - mock_integration(hass, MockModule(MODBUS_DOMAIN)) + mock_integration(hass, MockModule(DOMAIN)) hub = mock.MagicMock() hub.name = "hub" - hass.data[MODBUS_DOMAIN] = {DEFAULT_HUB: hub} + hass.data[DOMAIN] = {DEFAULT_HUB: hub} return hub From e0595ce518efc0a9bb3b2736d11fb51c3eaafc47 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Apr 2020 17:07:39 -0500 Subject: [PATCH 12/18] Fix tplink HS220 dimmers (#33909) * HS220 dimmers are handled as lights with a limited feature set --- homeassistant/components/tplink/light.py | 37 ++++- tests/components/tplink/test_light.py | 177 ++++++++++++++++++++++- 2 files changed, 210 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index fcb05bcdcea..ffafd1f6300 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -357,7 +357,7 @@ class TPLinkSmartBulb(Light): def _get_light_state(self) -> LightState: """Get the light state.""" self._update_emeter() - return self._light_state_from_params(self.smartbulb.get_light_state()) + return self._light_state_from_params(self._get_device_state()) def _update_emeter(self): if not self.smartbulb.has_emeter: @@ -439,7 +439,40 @@ class TPLinkSmartBulb(Light): if not diff: return - return self.smartbulb.set_light_state(diff) + return self._set_device_state(diff) + + def _get_device_state(self): + """State of the bulb or smart dimmer switch.""" + if isinstance(self.smartbulb, SmartBulb): + return self.smartbulb.get_light_state() + + # Its not really a bulb, its a dimmable SmartPlug (aka Wall Switch) + return { + LIGHT_STATE_ON_OFF: self.smartbulb.state, + LIGHT_STATE_BRIGHTNESS: self.smartbulb.brightness, + LIGHT_STATE_COLOR_TEMP: 0, + LIGHT_STATE_HUE: 0, + LIGHT_STATE_SATURATION: 0, + } + + def _set_device_state(self, state): + """Set state of the bulb or smart dimmer switch.""" + if isinstance(self.smartbulb, SmartBulb): + return self.smartbulb.set_light_state(state) + + # Its not really a bulb, its a dimmable SmartPlug (aka Wall Switch) + if LIGHT_STATE_BRIGHTNESS in state: + # Brightness of 0 is accepted by the + # device but the underlying library rejects it + # so we turn off instead. + if state[LIGHT_STATE_BRIGHTNESS]: + self.smartbulb.brightness = state[LIGHT_STATE_BRIGHTNESS] + else: + self.smartbulb.state = 0 + elif LIGHT_STATE_ON_OFF in state: + self.smartbulb.state = state[LIGHT_STATE_ON_OFF] + + return self._get_device_state() def _light_state_diff(old_light_state: LightState, new_light_state: LightState): diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index f6f27a888c5..09c23c6f0e5 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -1,6 +1,6 @@ """Tests for light platform.""" from typing import Callable, NamedTuple -from unittest.mock import Mock, patch +from unittest.mock import Mock, PropertyMock, patch from pyHS100 import SmartDeviceException import pytest @@ -16,7 +16,11 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, DOMAIN as LIGHT_DOMAIN, ) -from homeassistant.components.tplink.common import CONF_DISCOVERY, CONF_LIGHT +from homeassistant.components.tplink.common import ( + CONF_DIMMER, + CONF_DISCOVERY, + CONF_LIGHT, +) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, @@ -41,6 +45,16 @@ class LightMockData(NamedTuple): get_emeter_monthly_mock: Mock +class SmartSwitchMockData(NamedTuple): + """Mock smart switch data.""" + + sys_info: dict + light_state: dict + state_mock: Mock + brightness_mock: Mock + get_sysinfo_mock: Mock + + @pytest.fixture(name="light_mock_data") def light_mock_data_fixture() -> None: """Create light mock data.""" @@ -152,6 +166,75 @@ def light_mock_data_fixture() -> None: ) +@pytest.fixture(name="dimmer_switch_mock_data") +def dimmer_switch_mock_data_fixture() -> None: + """Create dimmer switch mock data.""" + sys_info = { + "sw_ver": "1.2.3", + "hw_ver": "2.3.4", + "mac": "aa:bb:cc:dd:ee:ff", + "mic_mac": "00:11:22:33:44", + "type": "switch", + "hwId": "1234", + "fwId": "4567", + "oemId": "891011", + "dev_name": "dimmer1", + "rssi": 11, + "latitude": "0", + "longitude": "0", + "is_color": False, + "is_dimmable": True, + "is_variable_color_temp": False, + "model": "HS220", + "alias": "dimmer1", + "feature": ":", + } + + light_state = { + "on_off": 1, + "brightness": 13, + } + + def state(*args, **kwargs): + nonlocal light_state + if len(args) == 0: + return light_state["on_off"] + light_state["on_off"] = args[0] + + def brightness(*args, **kwargs): + nonlocal light_state + if len(args) == 0: + return light_state["brightness"] + if light_state["brightness"] == 0: + light_state["on_off"] = 0 + else: + light_state["on_off"] = 1 + light_state["brightness"] = args[0] + + get_sysinfo_patch = patch( + "homeassistant.components.tplink.common.SmartDevice.get_sysinfo", + return_value=sys_info, + ) + state_patch = patch( + "homeassistant.components.tplink.common.SmartPlug.state", + new_callable=PropertyMock, + side_effect=state, + ) + brightness_patch = patch( + "homeassistant.components.tplink.common.SmartPlug.brightness", + new_callable=PropertyMock, + side_effect=brightness, + ) + with brightness_patch as brightness_mock, state_patch as state_mock, get_sysinfo_patch as get_sysinfo_mock: + yield SmartSwitchMockData( + sys_info=sys_info, + light_state=light_state, + brightness_mock=brightness_mock, + state_mock=state_mock, + get_sysinfo_mock=get_sysinfo_mock, + ) + + async def update_entity(hass: HomeAssistant, entity_id: str) -> None: """Run an update action for an entity.""" await hass.services.async_call( @@ -160,6 +243,96 @@ async def update_entity(hass: HomeAssistant, entity_id: str) -> None: await hass.async_block_till_done() +async def test_smartswitch( + hass: HomeAssistant, dimmer_switch_mock_data: SmartSwitchMockData +) -> None: + """Test function.""" + light_state = dimmer_switch_mock_data.light_state + + await async_setup_component(hass, HA_DOMAIN, {}) + await hass.async_block_till_done() + + await async_setup_component( + hass, + tplink.DOMAIN, + { + tplink.DOMAIN: { + CONF_DISCOVERY: False, + CONF_DIMMER: [{CONF_HOST: "123.123.123.123"}], + } + }, + ) + await hass.async_block_till_done() + + assert hass.states.get("light.dimmer1") + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.dimmer1"}, + blocking=True, + ) + await hass.async_block_till_done() + await update_entity(hass, "light.dimmer1") + + assert hass.states.get("light.dimmer1").state == "off" + assert light_state["on_off"] == 0 + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.dimmer1", ATTR_BRIGHTNESS: 50}, + blocking=True, + ) + await hass.async_block_till_done() + await update_entity(hass, "light.dimmer1") + + state = hass.states.get("light.dimmer1") + assert state.state == "on" + assert state.attributes["brightness"] == 48.45 + assert light_state["on_off"] == 1 + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.dimmer1", ATTR_BRIGHTNESS: 55}, + blocking=True, + ) + await hass.async_block_till_done() + await update_entity(hass, "light.dimmer1") + + state = hass.states.get("light.dimmer1") + assert state.state == "on" + assert state.attributes["brightness"] == 53.55 + assert light_state["brightness"] == 21 + + light_state["on_off"] = 0 + light_state["brightness"] = 66 + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.dimmer1"}, + blocking=True, + ) + await hass.async_block_till_done() + await update_entity(hass, "light.dimmer1") + + state = hass.states.get("light.dimmer1") + assert state.state == "off" + + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: "light.dimmer1"}, blocking=True, + ) + await hass.async_block_till_done() + await update_entity(hass, "light.dimmer1") + + state = hass.states.get("light.dimmer1") + assert state.state == "on" + assert state.attributes["brightness"] == 168.3 + assert light_state["brightness"] == 66 + + async def test_light(hass: HomeAssistant, light_mock_data: LightMockData) -> None: """Test function.""" light_state = light_mock_data.light_state From 3ad9052b5cf0c3d2d82c6d9b72bd3bd7039c364f Mon Sep 17 00:00:00 2001 From: Kit Klein <33464407+kit-klein@users.noreply.github.com> Date: Thu, 9 Apr 2020 19:16:33 -0400 Subject: [PATCH 13/18] Exclude access token from host info updates in Konnected config flow (#33912) * black updates * test that host update doesn't impact access token --- homeassistant/components/konnected/config_flow.py | 11 +++++------ tests/components/konnected/test_config_flow.py | 8 +++++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/konnected/config_flow.py b/homeassistant/components/konnected/config_flow.py index 6a3631a8c0d..a6b01560c50 100644 --- a/homeassistant/components/konnected/config_flow.py +++ b/homeassistant/components/konnected/config_flow.py @@ -283,11 +283,6 @@ class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # build config info and wait for user confirmation self.data[CONF_HOST] = user_input[CONF_HOST] self.data[CONF_PORT] = user_input[CONF_PORT] - self.data[CONF_ACCESS_TOKEN] = self.hass.data.get(DOMAIN, {}).get( - CONF_ACCESS_TOKEN - ) or "".join( - random.choices(f"{string.ascii_uppercase}{string.digits}", k=20) - ) # brief delay to allow processing of recent status req await asyncio.sleep(0.1) @@ -343,8 +338,12 @@ class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): }, ) - # Attach default options and create entry + # Create access token, attach default options and create entry self.data[CONF_DEFAULT_OPTIONS] = self.options + self.data[CONF_ACCESS_TOKEN] = self.hass.data.get(DOMAIN, {}).get( + CONF_ACCESS_TOKEN + ) or "".join(random.choices(f"{string.ascii_uppercase}{string.digits}", k=20)) + return self.async_create_entry( title=KONN_PANEL_MODEL_NAMES[self.data[CONF_MODEL]], data=self.data, ) diff --git a/tests/components/konnected/test_config_flow.py b/tests/components/konnected/test_config_flow.py index 917afc5357a..0bf6e7846ae 100644 --- a/tests/components/konnected/test_config_flow.py +++ b/tests/components/konnected/test_config_flow.py @@ -362,10 +362,11 @@ async def test_ssdp_host_update(hass, mock_panel): ) assert result["type"] == "abort" - # confirm the host value was updated + # confirm the host value was updated, access_token was not entry = hass.config_entries.async_entries(config_flow.DOMAIN)[0] assert entry.data["host"] == "1.1.1.1" assert entry.data["port"] == 1234 + assert entry.data["access_token"] == "11223344556677889900" async def test_import_existing_config(hass, mock_panel): @@ -494,6 +495,7 @@ async def test_import_existing_config_entry(hass, mock_panel): data={ "host": "0.0.0.0", "port": 1111, + "access_token": "ORIGINALTOKEN", "id": "112233445566", "extra": "something", }, @@ -546,14 +548,14 @@ async def test_import_existing_config_entry(hass, mock_panel): assert result["type"] == "abort" - # We should have updated the entry + # We should have updated the host info but not the access token assert len(hass.config_entries.async_entries("konnected")) == 1 assert hass.config_entries.async_entries("konnected")[0].data == { "host": "1.2.3.4", "port": 1234, + "access_token": "ORIGINALTOKEN", "id": "112233445566", "model": "Konnected Pro", - "access_token": "SUPERSECRETTOKEN", "extra": "something", } From f38ff3b622fe444c2205001d0a1c0e6170e805b1 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 9 Apr 2020 19:09:05 -0400 Subject: [PATCH 14/18] Bump pyvizio version for vizio (#33924) --- homeassistant/components/vizio/manifest.json | 3 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json index 885cfacca41..02904bedbde 100644 --- a/homeassistant/components/vizio/manifest.json +++ b/homeassistant/components/vizio/manifest.json @@ -2,8 +2,7 @@ "domain": "vizio", "name": "VIZIO SmartCast", "documentation": "https://www.home-assistant.io/integrations/vizio", - "requirements": ["pyvizio==0.1.44"], - "dependencies": [], + "requirements": ["pyvizio==0.1.46"], "codeowners": ["@raman325"], "config_flow": true, "zeroconf": ["_viziocast._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index a923602b107..0a121a00178 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1738,7 +1738,7 @@ pyversasense==0.0.6 pyvesync==1.1.0 # homeassistant.components.vizio -pyvizio==0.1.44 +pyvizio==0.1.46 # homeassistant.components.velux pyvlx==0.2.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c8368b00a80..f64d2f7c948 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -647,7 +647,7 @@ pyvera==0.3.7 pyvesync==1.1.0 # homeassistant.components.vizio -pyvizio==0.1.44 +pyvizio==0.1.46 # homeassistant.components.html5 pywebpush==1.9.2 From 1b0ccf10e51efae56222bee8f87b7cd7adc586e7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Apr 2020 19:10:02 -0500 Subject: [PATCH 15/18] Fix tplink HS220 dimmers (round 2) (#33928) * HS220 dimmers are handled as lights with a limited feature set * Dimmers look up has has_emeter every call so this is cached as well now to resovle the performance issue. --- homeassistant/components/tplink/light.py | 20 +++++++---- tests/components/tplink/test_light.py | 42 +++++++++++------------- 2 files changed, 34 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index ffafd1f6300..b1a79a03c8c 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -40,6 +40,7 @@ ATTR_MONTHLY_ENERGY_KWH = "monthly_energy_kwh" LIGHT_STATE_DFT_ON = "dft_on_state" LIGHT_STATE_ON_OFF = "on_off" +LIGHT_STATE_RELAY_STATE = "relay_state" LIGHT_STATE_BRIGHTNESS = "brightness" LIGHT_STATE_COLOR_TEMP = "color_temp" LIGHT_STATE_HUE = "hue" @@ -128,6 +129,7 @@ class LightFeatures(NamedTuple): supported_features: int min_mireds: float max_mireds: float + has_emeter: bool class TPLinkSmartBulb(Light): @@ -285,8 +287,9 @@ class TPLinkSmartBulb(Light): model = sysinfo[LIGHT_SYSINFO_MODEL] min_mireds = None max_mireds = None + has_emeter = self.smartbulb.has_emeter - if sysinfo.get(LIGHT_SYSINFO_IS_DIMMABLE): + if sysinfo.get(LIGHT_SYSINFO_IS_DIMMABLE) or LIGHT_STATE_BRIGHTNESS in sysinfo: supported_features += SUPPORT_BRIGHTNESS if sysinfo.get(LIGHT_SYSINFO_IS_VARIABLE_COLOR_TEMP): supported_features += SUPPORT_COLOR_TEMP @@ -306,6 +309,7 @@ class TPLinkSmartBulb(Light): supported_features=supported_features, min_mireds=min_mireds, max_mireds=max_mireds, + has_emeter=has_emeter, ) def _get_light_state_retry(self) -> LightState: @@ -360,7 +364,7 @@ class TPLinkSmartBulb(Light): return self._light_state_from_params(self._get_device_state()) def _update_emeter(self): - if not self.smartbulb.has_emeter: + if not self._light_features.has_emeter: return now = dt_util.utcnow() @@ -446,10 +450,11 @@ class TPLinkSmartBulb(Light): if isinstance(self.smartbulb, SmartBulb): return self.smartbulb.get_light_state() + sysinfo = self.smartbulb.sys_info # Its not really a bulb, its a dimmable SmartPlug (aka Wall Switch) return { - LIGHT_STATE_ON_OFF: self.smartbulb.state, - LIGHT_STATE_BRIGHTNESS: self.smartbulb.brightness, + LIGHT_STATE_ON_OFF: sysinfo[LIGHT_STATE_RELAY_STATE], + LIGHT_STATE_BRIGHTNESS: sysinfo.get(LIGHT_STATE_BRIGHTNESS, 0), LIGHT_STATE_COLOR_TEMP: 0, LIGHT_STATE_HUE: 0, LIGHT_STATE_SATURATION: 0, @@ -468,9 +473,12 @@ class TPLinkSmartBulb(Light): if state[LIGHT_STATE_BRIGHTNESS]: self.smartbulb.brightness = state[LIGHT_STATE_BRIGHTNESS] else: - self.smartbulb.state = 0 + self.smartbulb.state = self.smartbulb.SWITCH_STATE_OFF elif LIGHT_STATE_ON_OFF in state: - self.smartbulb.state = state[LIGHT_STATE_ON_OFF] + if state[LIGHT_STATE_ON_OFF]: + self.smartbulb.state = self.smartbulb.SWITCH_STATE_ON + else: + self.smartbulb.state = self.smartbulb.SWITCH_STATE_OFF return self._get_device_state() diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index 09c23c6f0e5..27d00024706 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -49,7 +49,6 @@ class SmartSwitchMockData(NamedTuple): """Mock smart switch data.""" sys_info: dict - light_state: dict state_mock: Mock brightness_mock: Mock get_sysinfo_mock: Mock @@ -188,28 +187,28 @@ def dimmer_switch_mock_data_fixture() -> None: "model": "HS220", "alias": "dimmer1", "feature": ":", - } - - light_state = { - "on_off": 1, + "relay_state": 1, "brightness": 13, } def state(*args, **kwargs): - nonlocal light_state + nonlocal sys_info if len(args) == 0: - return light_state["on_off"] - light_state["on_off"] = args[0] + return sys_info["relay_state"] + if args[0] == "ON": + sys_info["relay_state"] = 1 + else: + sys_info["relay_state"] = 0 def brightness(*args, **kwargs): - nonlocal light_state + nonlocal sys_info if len(args) == 0: - return light_state["brightness"] - if light_state["brightness"] == 0: - light_state["on_off"] = 0 + return sys_info["brightness"] + if sys_info["brightness"] == 0: + sys_info["relay_state"] = 0 else: - light_state["on_off"] = 1 - light_state["brightness"] = args[0] + sys_info["relay_state"] = 1 + sys_info["brightness"] = args[0] get_sysinfo_patch = patch( "homeassistant.components.tplink.common.SmartDevice.get_sysinfo", @@ -228,7 +227,6 @@ def dimmer_switch_mock_data_fixture() -> None: with brightness_patch as brightness_mock, state_patch as state_mock, get_sysinfo_patch as get_sysinfo_mock: yield SmartSwitchMockData( sys_info=sys_info, - light_state=light_state, brightness_mock=brightness_mock, state_mock=state_mock, get_sysinfo_mock=get_sysinfo_mock, @@ -247,7 +245,7 @@ async def test_smartswitch( hass: HomeAssistant, dimmer_switch_mock_data: SmartSwitchMockData ) -> None: """Test function.""" - light_state = dimmer_switch_mock_data.light_state + sys_info = dimmer_switch_mock_data.sys_info await async_setup_component(hass, HA_DOMAIN, {}) await hass.async_block_till_done() @@ -276,7 +274,7 @@ async def test_smartswitch( await update_entity(hass, "light.dimmer1") assert hass.states.get("light.dimmer1").state == "off" - assert light_state["on_off"] == 0 + assert sys_info["relay_state"] == 0 await hass.services.async_call( LIGHT_DOMAIN, @@ -290,7 +288,7 @@ async def test_smartswitch( state = hass.states.get("light.dimmer1") assert state.state == "on" assert state.attributes["brightness"] == 48.45 - assert light_state["on_off"] == 1 + assert sys_info["relay_state"] == 1 await hass.services.async_call( LIGHT_DOMAIN, @@ -304,10 +302,10 @@ async def test_smartswitch( state = hass.states.get("light.dimmer1") assert state.state == "on" assert state.attributes["brightness"] == 53.55 - assert light_state["brightness"] == 21 + assert sys_info["brightness"] == 21 - light_state["on_off"] = 0 - light_state["brightness"] = 66 + sys_info["relay_state"] = 0 + sys_info["brightness"] = 66 await hass.services.async_call( LIGHT_DOMAIN, @@ -330,7 +328,7 @@ async def test_smartswitch( state = hass.states.get("light.dimmer1") assert state.state == "on" assert state.attributes["brightness"] == 168.3 - assert light_state["brightness"] == 66 + assert sys_info["brightness"] == 66 async def test_light(hass: HomeAssistant, light_mock_data: LightMockData) -> None: From 3100e852cef18f113423c2e576a0a78bf91eaf30 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 9 Apr 2020 17:23:21 -0700 Subject: [PATCH 16/18] Bumped version to 0.108.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index f9413cf6e4d..fb09e467c47 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 108 -PATCH_VERSION = "1" +PATCH_VERSION = "2" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From 8259a5a71ffbb95cb110b6ed88ebef8d34aa48c2 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 9 Apr 2020 19:44:04 -0500 Subject: [PATCH 17/18] Guard IPP against negative ink levels (#33931) --- homeassistant/components/ipp/sensor.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index 1ce162500c5..fd278d3df2e 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -116,7 +116,12 @@ class IPPMarkerSensor(IPPSensor): @property def state(self) -> Union[None, str, int, float]: """Return the state of the sensor.""" - return self.coordinator.data.markers[self.marker_index].level + level = self.coordinator.data.markers[self.marker_index].level + + if level >= 0: + return level + + return None class IPPPrinterSensor(IPPSensor): From 3331b81b64fc910a88d7db5cfa1cd602cc9761a9 Mon Sep 17 00:00:00 2001 From: Carlos Gustavo Sarmiento Date: Thu, 9 Apr 2020 12:37:53 -0700 Subject: [PATCH 18/18] Remove print() from Bayesian Binary Sensor (#33916) --- homeassistant/components/bayesian/binary_sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index b922c966ff5..74a0aaae295 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -298,8 +298,6 @@ class BayesianBinarySensor(BinarySensorDevice): @property def device_state_attributes(self): """Return the state attributes of the sensor.""" - print(self.current_observations) - print(self.observations_by_entity) return { ATTR_OBSERVATIONS: list(self.current_observations.values()), ATTR_OCCURRED_OBSERVATION_ENTITIES: list(