From ed8e8cbdefc8eb0f257337648bde8861837cfae9 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 3 Sep 2024 18:24:08 +0000 Subject: [PATCH 1/2] Add template to command args in command_line notify --- .../components/command_line/notify.py | 33 ++++++++++++++++--- tests/components/command_line/test_notify.py | 31 +++++++++++++++++ 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/command_line/notify.py b/homeassistant/components/command_line/notify.py index 4f5a4e4b499..aa19b8a9936 100644 --- a/homeassistant/components/command_line/notify.py +++ b/homeassistant/components/command_line/notify.py @@ -9,10 +9,12 @@ from typing import Any, cast from homeassistant.components.notify import BaseNotificationService from homeassistant.const import CONF_COMMAND from homeassistant.core import HomeAssistant +from homeassistant.exceptions import TemplateError +from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.process import kill_subprocess -from .const import CONF_COMMAND_TIMEOUT +from .const import CONF_COMMAND_TIMEOUT, LOGGER _LOGGER = logging.getLogger(__name__) @@ -44,8 +46,29 @@ class CommandLineNotificationService(BaseNotificationService): def send_message(self, message: str = "", **kwargs: Any) -> None: """Send a message to a command line.""" + command = self.command + if " " not in command: + prog = command + args = None + args_compiled = None + else: + prog, args = command.split(" ", 1) + args_compiled = Template(args, self.hass) + + rendered_args = None + if args_compiled: + args_to_render = {"arguments": args} + try: + rendered_args = args_compiled.async_render(args_to_render) + except TemplateError as ex: + LOGGER.exception("Error rendering command template: %s", ex) + return + + if rendered_args != args: + command = f"{prog} {rendered_args}" + with subprocess.Popen( # noqa: S602 # shell by design - self.command, + command, universal_newlines=True, stdin=subprocess.PIPE, close_fds=False, # required for posix_spawn @@ -57,10 +80,10 @@ class CommandLineNotificationService(BaseNotificationService): _LOGGER.error( "Command failed (with return code %s): %s", proc.returncode, - self.command, + command, ) except subprocess.TimeoutExpired: - _LOGGER.error("Timeout for command: %s", self.command) + _LOGGER.error("Timeout for command: %s", command) kill_subprocess(proc) except subprocess.SubprocessError: - _LOGGER.error("Error trying to exec command: %s", self.command) + _LOGGER.error("Error trying to exec command: %s", command) diff --git a/tests/components/command_line/test_notify.py b/tests/components/command_line/test_notify.py index 6898b44f062..21154d3c851 100644 --- a/tests/components/command_line/test_notify.py +++ b/tests/components/command_line/test_notify.py @@ -100,6 +100,37 @@ async def test_command_line_output(hass: HomeAssistant) -> None: assert message == await hass.async_add_executor_job(Path(filename).read_text) +async def test_command_template(hass: HomeAssistant) -> None: + """Test the command line output using template as command.""" + + with tempfile.TemporaryDirectory() as tempdirname: + filename = os.path.join(tempdirname, "message.txt") + message = "one, two, testing, testing" + hass.states.async_set("sensor.test_state", filename) + await setup.async_setup_component( + hass, + DOMAIN, + { + "command_line": [ + { + "notify": { + "command": "cat > {{ states.sensor.test_state.state }}", + "name": "Test3", + } + } + ] + }, + ) + await hass.async_block_till_done() + + assert hass.services.has_service(NOTIFY_DOMAIN, "test3") + + await hass.services.async_call( + NOTIFY_DOMAIN, "test3", {"message": message}, blocking=True + ) + assert message == await hass.async_add_executor_job(Path(filename).read_text) + + @pytest.mark.parametrize( "get_config", [ From 7c103ca9ae511d1ac9f6c6960072bf9e36b46955 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 3 Sep 2024 18:51:26 +0000 Subject: [PATCH 2/2] coverage --- .../components/command_line/notify.py | 2 + tests/components/command_line/test_notify.py | 63 +++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/homeassistant/components/command_line/notify.py b/homeassistant/components/command_line/notify.py index aa19b8a9936..861a406c684 100644 --- a/homeassistant/components/command_line/notify.py +++ b/homeassistant/components/command_line/notify.py @@ -67,6 +67,8 @@ class CommandLineNotificationService(BaseNotificationService): if rendered_args != args: command = f"{prog} {rendered_args}" + LOGGER.debug("Running command: %s, with message: %s", command, message) + with subprocess.Popen( # noqa: S602 # shell by design command, universal_newlines=True, diff --git a/tests/components/command_line/test_notify.py b/tests/components/command_line/test_notify.py index 21154d3c851..a0c69765c9a 100644 --- a/tests/components/command_line/test_notify.py +++ b/tests/components/command_line/test_notify.py @@ -100,6 +100,35 @@ async def test_command_line_output(hass: HomeAssistant) -> None: assert message == await hass.async_add_executor_job(Path(filename).read_text) +async def test_command_line_output_single_command( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test the command line output.""" + + await setup.async_setup_component( + hass, + DOMAIN, + { + "command_line": [ + { + "notify": { + "command": "echo", + "name": "Test3", + } + } + ] + }, + ) + await hass.async_block_till_done() + + assert hass.services.has_service(NOTIFY_DOMAIN, "test3") + + await hass.services.async_call( + NOTIFY_DOMAIN, "test3", {"message": "test message"}, blocking=True + ) + assert "Running command: echo, with message: test message" in caplog.text + + async def test_command_template(hass: HomeAssistant) -> None: """Test the command line output using template as command.""" @@ -131,6 +160,40 @@ async def test_command_template(hass: HomeAssistant) -> None: assert message == await hass.async_add_executor_job(Path(filename).read_text) +async def test_command_incorrect_template( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test the command line output using template as command which isn't working.""" + + message = "one, two, testing, testing" + await setup.async_setup_component( + hass, + DOMAIN, + { + "command_line": [ + { + "notify": { + "command": "cat > {{ this template doesn't parse ", + "name": "Test3", + } + } + ] + }, + ) + await hass.async_block_till_done() + + assert hass.services.has_service(NOTIFY_DOMAIN, "test3") + + await hass.services.async_call( + NOTIFY_DOMAIN, "test3", {"message": message}, blocking=True + ) + + assert ( + "Error rendering command template: TemplateSyntaxError: expected token" + in caplog.text + ) + + @pytest.mark.parametrize( "get_config", [