diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 746ab3adc03..db97c3a321a 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -39,6 +39,7 @@ from homeassistant.helpers.script import ( ATTR_MAX, ATTR_MODE, CONF_MAX, + CONF_MAX_EXCEEDED, SCRIPT_MODE_SINGLE, Script, make_script_schema, @@ -515,6 +516,7 @@ async def _async_process_config(hass, config, component): running_description="automation actions", script_mode=config_block[CONF_MODE], max_runs=config_block[CONF_MAX], + max_exceeded=config_block[CONF_MAX_EXCEEDED], logger=_LOGGER, ) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index b6d02872adb..20f12361621 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -28,6 +28,7 @@ from homeassistant.helpers.script import ( ATTR_MAX, ATTR_MODE, CONF_MAX, + CONF_MAX_EXCEEDED, SCRIPT_MODE_SINGLE, Script, make_script_schema, @@ -260,6 +261,7 @@ class ScriptEntity(ToggleEntity): change_listener=self.async_change_listener, script_mode=cfg[CONF_MODE], max_runs=cfg[CONF_MAX], + max_exceeded=cfg[CONF_MAX_EXCEEDED], logger=logging.getLogger(f"{__name__}.{object_id}"), ) self._changed = asyncio.Event() diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index adbacda6742..d4d9c11fa71 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -24,6 +24,7 @@ import voluptuous as vol from homeassistant import exceptions import homeassistant.components.device_automation as device_automation +from homeassistant.components.logger import LOGSEVERITY import homeassistant.components.scene as scene from homeassistant.const import ( ATTR_ENTITY_ID, @@ -88,6 +89,10 @@ DEFAULT_SCRIPT_MODE = SCRIPT_MODE_SINGLE CONF_MAX = "max" DEFAULT_MAX = 10 +CONF_MAX_EXCEEDED = "max_exceeded" +_MAX_EXCEEDED_CHOICES = list(LOGSEVERITY) + ["SILENT"] +DEFAULT_MAX_EXCEEDED = "WARNING" + ATTR_CUR = "current" ATTR_MAX = "max" ATTR_MODE = "mode" @@ -113,6 +118,9 @@ def make_script_schema(schema, default_script_mode, extra=vol.PREVENT_EXTRA): vol.Optional(CONF_MAX, default=DEFAULT_MAX): vol.All( vol.Coerce(int), vol.Range(min=2) ), + vol.Optional(CONF_MAX_EXCEEDED, default=DEFAULT_MAX_EXCEEDED): vol.All( + vol.Upper, vol.In(_MAX_EXCEEDED_CHOICES) + ), }, extra=extra, ) @@ -710,6 +718,7 @@ class Script: change_listener: Optional[Callable[..., Any]] = None, script_mode: str = DEFAULT_SCRIPT_MODE, max_runs: int = DEFAULT_MAX, + max_exceeded: str = DEFAULT_MAX_EXCEEDED, logger: Optional[logging.Logger] = None, log_exceptions: bool = True, top_level: bool = True, @@ -743,6 +752,7 @@ class Script: self._runs: List[_ScriptRun] = [] self.max_runs = max_runs + self._max_exceeded = max_exceeded if script_mode == SCRIPT_MODE_QUEUED: self._queue_lck = asyncio.Lock() self._config_cache: Dict[Set[Tuple], Callable[..., bool]] = {} @@ -871,13 +881,18 @@ class Script: if self.is_running: if self.script_mode == SCRIPT_MODE_SINGLE: - self._log("Already running", level=logging.WARNING) + if self._max_exceeded != "SILENT": + self._log("Already running", level=LOGSEVERITY[self._max_exceeded]) return if self.script_mode == SCRIPT_MODE_RESTART: self._log("Restarting") await self.async_stop(update_state=False) elif len(self._runs) == self.max_runs: - self._log("Maximum number of runs exceeded", level=logging.WARNING) + if self._max_exceeded != "SILENT": + self._log( + "Maximum number of runs exceeded", + level=LOGSEVERITY[self._max_exceeded], + ) return # If this is a top level Script then make a copy of the variables in case they diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 364d635f506..e997e3e92d6 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1419,6 +1419,60 @@ async def test_script_mode_single(hass, caplog): assert events[1].data["value"] == 2 +@pytest.mark.parametrize("max_exceeded", [None, "WARNING", "INFO", "ERROR", "SILENT"]) +@pytest.mark.parametrize( + "script_mode,max_runs", [("single", 1), ("parallel", 2), ("queued", 2)] +) +async def test_max_exceeded(hass, caplog, max_exceeded, script_mode, max_runs): + """Test max_exceeded option.""" + sequence = cv.SCRIPT_SCHEMA( + {"wait_template": "{{ states.switch.test.state == 'off' }}"} + ) + if max_exceeded is None: + script_obj = script.Script( + hass, + sequence, + "Test Name", + "test_domain", + script_mode=script_mode, + max_runs=max_runs, + ) + else: + script_obj = script.Script( + hass, + sequence, + "Test Name", + "test_domain", + script_mode=script_mode, + max_runs=max_runs, + max_exceeded=max_exceeded, + ) + hass.states.async_set("switch.test", "on") + for _ in range(max_runs + 1): + hass.async_create_task(script_obj.async_run(context=Context())) + hass.states.async_set("switch.test", "off") + await hass.async_block_till_done() + if max_exceeded is None: + max_exceeded = "WARNING" + if max_exceeded == "SILENT": + assert not any( + any( + message in rec.message + for message in ("Already running", "Maximum number of runs exceeded") + ) + for rec in caplog.records + ) + else: + assert any( + rec.levelname == max_exceeded + and any( + message in rec.message + for message in ("Already running", "Maximum number of runs exceeded") + ) + for rec in caplog.records + ) + + @pytest.mark.parametrize( "script_mode,messages,last_events", [("restart", ["Restarting"], [2]), ("parallel", [], [2, 2])],