From 4821484d2c34ee2f1b3c4e200e85e950844fc2e0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 31 May 2021 15:36:40 -0700 Subject: [PATCH] Add system option to disable polling (#51299) --- .../components/config/config_entries.py | 36 +++++---- homeassistant/config_entries.py | 27 +++++-- homeassistant/helpers/entity_platform.py | 7 +- homeassistant/helpers/update_coordinator.py | 9 ++- .../components/config/test_config_entries.py | 73 +++++++++++++------ tests/helpers/test_entity_platform.py | 13 ++++ tests/helpers/test_update_coordinator.py | 12 ++- 7 files changed, 123 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index efc60288439..9d88a9b5311 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -31,7 +31,6 @@ async def async_setup(hass): hass.components.websocket_api.async_register_command(config_entry_disable) hass.components.websocket_api.async_register_command(config_entry_update) hass.components.websocket_api.async_register_command(config_entries_progress) - hass.components.websocket_api.async_register_command(system_options_list) hass.components.websocket_api.async_register_command(system_options_update) hass.components.websocket_api.async_register_command(ignore_config_flow) @@ -231,20 +230,6 @@ def config_entries_progress(hass, connection, msg): ) -@websocket_api.require_admin -@websocket_api.async_response -@websocket_api.websocket_command( - {"type": "config_entries/system_options/list", "entry_id": str} -) -async def system_options_list(hass, connection, msg): - """List all system options for a config entry.""" - entry_id = msg["entry_id"] - entry = hass.config_entries.async_get_entry(entry_id) - - if entry: - connection.send_result(msg["id"], entry.system_options.as_dict()) - - def send_entry_not_found(connection, msg_id): """Send Config entry not found error.""" connection.send_error( @@ -267,6 +252,7 @@ def get_entry(hass, connection, entry_id, msg_id): "type": "config_entries/system_options/update", "entry_id": str, vol.Optional("disable_new_entities"): bool, + vol.Optional("disable_polling"): bool, } ) async def system_options_update(hass, connection, msg): @@ -280,8 +266,25 @@ async def system_options_update(hass, connection, msg): if entry is None: return + old_disable_polling = entry.system_options.disable_polling + hass.config_entries.async_update_entry(entry, system_options=changes) - connection.send_result(msg["id"], entry.system_options.as_dict()) + + result = { + "system_options": entry.system_options.as_dict(), + "require_restart": False, + } + + if ( + old_disable_polling != entry.system_options.disable_polling + and entry.state is config_entries.ConfigEntryState.LOADED + ): + if not await hass.config_entries.async_reload(entry.entry_id): + result["require_restart"] = ( + entry.state is config_entries.ConfigEntryState.FAILED_UNLOAD + ) + + connection.send_result(msg["id"], result) @websocket_api.require_admin @@ -388,6 +391,7 @@ def entry_json(entry: config_entries.ConfigEntry) -> dict: "state": entry.state.value, "supports_options": supports_options, "supports_unload": entry.supports_unload, + "system_options": entry.system_options.as_dict(), "disabled_by": entry.disabled_by, "reason": entry.reason, } diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index b3589d03b92..33c18fc0d7c 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -994,12 +994,10 @@ class ConfigEntries: changed = True entry.options = MappingProxyType(options) - if ( - system_options is not UNDEFINED - and entry.system_options.as_dict() != system_options - ): - changed = True + if system_options is not UNDEFINED: + old_system_options = entry.system_options.as_dict() entry.system_options.update(**system_options) + changed = entry.system_options.as_dict() != old_system_options if not changed: return False @@ -1408,14 +1406,27 @@ class SystemOptions: """Config entry system options.""" disable_new_entities: bool = attr.ib(default=False) + disable_polling: bool = attr.ib(default=False) - def update(self, *, disable_new_entities: bool) -> None: + def update( + self, + *, + disable_new_entities: bool | UndefinedType = UNDEFINED, + disable_polling: bool | UndefinedType = UNDEFINED, + ) -> None: """Update properties.""" - self.disable_new_entities = disable_new_entities + if disable_new_entities is not UNDEFINED: + self.disable_new_entities = disable_new_entities + + if disable_polling is not UNDEFINED: + self.disable_polling = disable_polling def as_dict(self) -> dict[str, Any]: """Return dictionary version of this config entries system options.""" - return {"disable_new_entities": self.disable_new_entities} + return { + "disable_new_entities": self.disable_new_entities, + "disable_polling": self.disable_polling, + } class EntityRegistryDisabledHandler: diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 26bfdb43e66..f0d691a1c8d 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -214,6 +214,7 @@ class EntityPlatform: @callback def async_create_setup_task() -> Coroutine: """Get task to set up platform.""" + config_entries.current_entry.set(config_entry) return platform.async_setup_entry( # type: ignore[no-any-return,union-attr] self.hass, config_entry, self._async_schedule_add_entities ) @@ -395,8 +396,10 @@ class EntityPlatform: ) raise - if self._async_unsub_polling is not None or not any( - entity.should_poll for entity in self.entities.values() + if ( + (self.config_entry and self.config_entry.system_options.disable_polling) + or self._async_unsub_polling is not None + or not any(entity.should_poll for entity in self.entities.values()) ): return diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index c15d6534626..e91acfaf82f 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -49,6 +49,7 @@ class DataUpdateCoordinator(Generic[T]): self.name = name self.update_method = update_method self.update_interval = update_interval + self.config_entry = config_entries.current_entry.get() # It's None before the first successful update. # Components should call async_config_entry_first_refresh @@ -110,6 +111,9 @@ class DataUpdateCoordinator(Generic[T]): if self.update_interval is None: return + if self.config_entry and self.config_entry.system_options.disable_polling: + return + if self._unsub_refresh: self._unsub_refresh() self._unsub_refresh = None @@ -229,9 +233,8 @@ class DataUpdateCoordinator(Generic[T]): if raise_on_auth_failed: raise - config_entry = config_entries.current_entry.get() - if config_entry: - config_entry.async_start_reauth(self.hass) + if self.config_entry: + self.config_entry.async_start_reauth(self.hass) except NotImplementedError as err: self.last_exception = err raise err diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 10fc3aadba0..570d847e86e 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -87,6 +87,10 @@ async def test_get_entries(hass, client): "state": core_ce.ConfigEntryState.NOT_LOADED.value, "supports_options": True, "supports_unload": True, + "system_options": { + "disable_new_entities": False, + "disable_polling": False, + }, "disabled_by": None, "reason": None, }, @@ -97,6 +101,10 @@ async def test_get_entries(hass, client): "state": core_ce.ConfigEntryState.SETUP_ERROR.value, "supports_options": False, "supports_unload": False, + "system_options": { + "disable_new_entities": False, + "disable_polling": False, + }, "disabled_by": None, "reason": "Unsupported API", }, @@ -107,6 +115,10 @@ async def test_get_entries(hass, client): "state": core_ce.ConfigEntryState.NOT_LOADED.value, "supports_options": False, "supports_unload": False, + "system_options": { + "disable_new_entities": False, + "disable_polling": False, + }, "disabled_by": core_ce.DISABLED_USER, "reason": None, }, @@ -328,6 +340,10 @@ async def test_create_account(hass, client, enable_custom_integrations): "state": core_ce.ConfigEntryState.LOADED.value, "supports_options": False, "supports_unload": False, + "system_options": { + "disable_new_entities": False, + "disable_polling": False, + }, "title": "Test Entry", "reason": None, }, @@ -399,6 +415,10 @@ async def test_two_step_flow(hass, client, enable_custom_integrations): "state": core_ce.ConfigEntryState.LOADED.value, "supports_options": False, "supports_unload": False, + "system_options": { + "disable_new_entities": False, + "disable_polling": False, + }, "title": "user-title", "reason": None, }, @@ -678,35 +698,17 @@ async def test_two_step_options_flow(hass, client): } -async def test_list_system_options(hass, hass_ws_client): - """Test that we can list an entries system options.""" - assert await async_setup_component(hass, "config", {}) - ws_client = await hass_ws_client(hass) - - entry = MockConfigEntry(domain="demo") - entry.add_to_hass(hass) - - await ws_client.send_json( - { - "id": 5, - "type": "config_entries/system_options/list", - "entry_id": entry.entry_id, - } - ) - response = await ws_client.receive_json() - - assert response["success"] - assert response["result"] == {"disable_new_entities": False} - - async def test_update_system_options(hass, hass_ws_client): """Test that we can update system options.""" assert await async_setup_component(hass, "config", {}) ws_client = await hass_ws_client(hass) - entry = MockConfigEntry(domain="demo") + entry = MockConfigEntry(domain="demo", state=core_ce.ConfigEntryState.LOADED) entry.add_to_hass(hass) + assert entry.system_options.disable_new_entities is False + assert entry.system_options.disable_polling is False + await ws_client.send_json( { "id": 5, @@ -718,8 +720,31 @@ async def test_update_system_options(hass, hass_ws_client): response = await ws_client.receive_json() assert response["success"] - assert response["result"]["disable_new_entities"] - assert entry.system_options.disable_new_entities + assert response["result"] == { + "require_restart": False, + "system_options": {"disable_new_entities": True, "disable_polling": False}, + } + assert entry.system_options.disable_new_entities is True + assert entry.system_options.disable_polling is False + + await ws_client.send_json( + { + "id": 6, + "type": "config_entries/system_options/update", + "entry_id": entry.entry_id, + "disable_new_entities": False, + "disable_polling": True, + } + ) + response = await ws_client.receive_json() + + assert response["success"] + assert response["result"] == { + "require_restart": True, + "system_options": {"disable_new_entities": False, "disable_polling": True}, + } + assert entry.system_options.disable_new_entities is False + assert entry.system_options.disable_polling is True async def test_update_system_options_nonexisting(hass, hass_ws_client): diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 6675e441adf..944f02d46c0 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -57,6 +57,19 @@ async def test_polling_only_updates_entities_it_should_poll(hass): assert poll_ent.async_update.called +async def test_polling_disabled_by_config_entry(hass): + """Test the polling of only updated entities.""" + entity_platform = MockEntityPlatform(hass) + entity_platform.config_entry = MockConfigEntry( + system_options={"disable_polling": True} + ) + + poll_ent = MockEntity(should_poll=True) + + await entity_platform.async_add_entities([poll_ent]) + assert entity_platform._async_unsub_polling is None + + async def test_polling_updates_entities_with_exception(hass): """Test the updated entities that not break with an exception.""" component = EntityComponent(_LOGGER, DOMAIN, hass, timedelta(seconds=20)) diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index 244e221f53a..a0ce751aed8 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -9,13 +9,14 @@ import aiohttp import pytest import requests +from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import CoreState from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import update_coordinator from homeassistant.util.dt import utcnow -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed _LOGGER = logging.getLogger(__name__) @@ -371,3 +372,12 @@ async def test_async_config_entry_first_refresh_success(crd, caplog): await crd.async_config_entry_first_refresh() assert crd.last_update_success is True + + +async def test_not_schedule_refresh_if_system_option_disable_polling(hass): + """Test we do not schedule a refresh if disable polling in config entry.""" + entry = MockConfigEntry(system_options={"disable_polling": True}) + config_entries.current_entry.set(entry) + crd = get_crd(hass, DEFAULT_UPDATE_INTERVAL) + crd.async_add_listener(lambda: None) + assert crd._unsub_refresh is None