Skip to content

Commit f4e5036

Browse files
New helper for templating args in command_line (home-assistant#145899)
1 parent 59aba33 commit f4e5036

File tree

4 files changed

+48
-62
lines changed

4 files changed

+48
-62
lines changed

homeassistant/components/command_line/notify.py

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,11 @@
99
from homeassistant.components.notify import BaseNotificationService
1010
from homeassistant.const import CONF_COMMAND
1111
from homeassistant.core import HomeAssistant
12-
from homeassistant.exceptions import TemplateError
13-
from homeassistant.helpers.template import Template
1412
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
1513
from homeassistant.util.process import kill_subprocess
1614

1715
from .const import CONF_COMMAND_TIMEOUT, LOGGER
16+
from .utils import render_template_args
1817

1918
_LOGGER = logging.getLogger(__name__)
2019

@@ -45,28 +44,10 @@ def __init__(self, command: str, timeout: int) -> None:
4544

4645
def send_message(self, message: str = "", **kwargs: Any) -> None:
4746
"""Send a message to a command line."""
48-
command = self.command
49-
if " " not in command:
50-
prog = command
51-
args = None
52-
args_compiled = None
53-
else:
54-
prog, args = command.split(" ", 1)
55-
args_compiled = Template(args, self.hass)
47+
if not (command := render_template_args(self.hass, self.command)):
48+
return
5649

57-
rendered_args = None
58-
if args_compiled:
59-
args_to_render = {"arguments": args}
60-
try:
61-
rendered_args = args_compiled.async_render(args_to_render)
62-
except TemplateError as ex:
63-
LOGGER.exception("Error rendering command template: %s", ex)
64-
return
65-
66-
if rendered_args != args:
67-
command = f"{prog} {rendered_args}"
68-
69-
LOGGER.debug("Running command: %s, with message: %s", command, message)
50+
LOGGER.debug("Running with message: %s", message)
7051

7152
with subprocess.Popen( # noqa: S602 # shell by design
7253
command,

homeassistant/components/command_line/sensor.py

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
CONF_VALUE_TEMPLATE,
2020
)
2121
from homeassistant.core import HomeAssistant
22-
from homeassistant.exceptions import TemplateError
2322
from homeassistant.helpers.entity_platform import AddEntitiesCallback
2423
from homeassistant.helpers.event import async_track_time_interval
2524
from homeassistant.helpers.template import Template
@@ -37,7 +36,7 @@
3736
LOGGER,
3837
TRIGGER_ENTITY_OPTIONS,
3938
)
40-
from .utils import async_check_output_or_log
39+
from .utils import async_check_output_or_log, render_template_args
4140

4241
DEFAULT_NAME = "Command Sensor"
4342

@@ -222,32 +221,6 @@ def __init__(self, hass: HomeAssistant, command: str, command_timeout: int) -> N
222221

223222
async def async_update(self) -> None:
224223
"""Get the latest data with a shell command."""
225-
command = self.command
226-
227-
if " " not in command:
228-
prog = command
229-
args = None
230-
args_compiled = None
231-
else:
232-
prog, args = command.split(" ", 1)
233-
args_compiled = Template(args, self.hass)
234-
235-
if args_compiled:
236-
try:
237-
args_to_render = {"arguments": args}
238-
rendered_args = args_compiled.async_render(args_to_render)
239-
except TemplateError as ex:
240-
LOGGER.exception("Error rendering command template: %s", ex)
241-
return
242-
else:
243-
rendered_args = None
244-
245-
if rendered_args == args:
246-
# No template used. default behavior
247-
pass
248-
else:
249-
# Template used. Construct the string used in the shell
250-
command = f"{prog} {rendered_args}"
251-
252-
LOGGER.debug("Running command: %s", command)
224+
if not (command := render_template_args(self.hass, self.command)):
225+
return
253226
self.value = await async_check_output_or_log(command, self.timeout)

homeassistant/components/command_line/utils.py

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33
from __future__ import annotations
44

55
import asyncio
6-
import logging
76

8-
_LOGGER = logging.getLogger(__name__)
7+
from homeassistant.core import HomeAssistant
8+
from homeassistant.exceptions import TemplateError
9+
from homeassistant.helpers.template import Template
10+
11+
from .const import LOGGER
12+
913
_EXEC_FAILED_CODE = 127
1014

1115

@@ -18,22 +22,22 @@ async def async_call_shell_with_timeout(
1822
return code is returned.
1923
"""
2024
try:
21-
_LOGGER.debug("Running command: %s", command)
25+
LOGGER.debug("Running command: %s", command)
2226
proc = await asyncio.create_subprocess_shell( # shell by design
2327
command,
2428
close_fds=False, # required for posix_spawn
2529
)
2630
async with asyncio.timeout(timeout):
2731
await proc.communicate()
2832
except TimeoutError:
29-
_LOGGER.error("Timeout for command: %s", command)
33+
LOGGER.error("Timeout for command: %s", command)
3034
return -1
3135

3236
return_code = proc.returncode
3337
if return_code == _EXEC_FAILED_CODE:
34-
_LOGGER.error("Error trying to exec command: %s", command)
38+
LOGGER.error("Error trying to exec command: %s", command)
3539
elif log_return_code and return_code != 0:
36-
_LOGGER.error(
40+
LOGGER.error(
3741
"Command failed (with return code %s): %s",
3842
proc.returncode,
3943
command,
@@ -53,12 +57,39 @@ async def async_check_output_or_log(command: str, timeout: int) -> str | None:
5357
stdout, _ = await proc.communicate()
5458

5559
if proc.returncode != 0:
56-
_LOGGER.error(
60+
LOGGER.error(
5761
"Command failed (with return code %s): %s", proc.returncode, command
5862
)
5963
else:
6064
return stdout.strip().decode("utf-8")
6165
except TimeoutError:
62-
_LOGGER.error("Timeout for command: %s", command)
66+
LOGGER.error("Timeout for command: %s", command)
6367

6468
return None
69+
70+
71+
def render_template_args(hass: HomeAssistant, command: str) -> str | None:
72+
"""Render template arguments for command line utilities."""
73+
if " " not in command:
74+
prog = command
75+
args = None
76+
args_compiled = None
77+
else:
78+
prog, args = command.split(" ", 1)
79+
args_compiled = Template(args, hass)
80+
81+
rendered_args = None
82+
if args_compiled:
83+
args_to_render = {"arguments": args}
84+
try:
85+
rendered_args = args_compiled.async_render(args_to_render)
86+
except TemplateError as ex:
87+
LOGGER.exception("Error rendering command template: %s", ex)
88+
return None
89+
90+
if rendered_args != args:
91+
command = f"{prog} {rendered_args}"
92+
93+
LOGGER.debug("Running command: %s", command)
94+
95+
return command

tests/components/command_line/test_notify.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,8 @@ async def test_command_line_output_single_command(
126126
await hass.services.async_call(
127127
NOTIFY_DOMAIN, "test3", {"message": "test message"}, blocking=True
128128
)
129-
assert "Running command: echo, with message: test message" in caplog.text
129+
assert "Running command: echo" in caplog.text
130+
assert "Running with message: test message" in caplog.text
130131

131132

132133
async def test_command_template(hass: HomeAssistant) -> None:

0 commit comments

Comments
 (0)