Convert command_line to use asyncio for subprocesses (#111927)
* Convert command_line to use asyncio for subprocesses * fixes * fix * fixes * more test fixes * more fixes * fixes * preen
This commit is contained in:
parent
5f65315e86
commit
c0f7ade92b
10 changed files with 115 additions and 117 deletions
|
@ -148,7 +148,7 @@ class CommandBinarySensor(ManualTriggerEntity, BinarySensorEntity):
|
|||
|
||||
async def _async_update(self) -> None:
|
||||
"""Get the latest data and updates the state."""
|
||||
await self.hass.async_add_executor_job(self.data.update)
|
||||
await self.data.async_update()
|
||||
value = self.data.value
|
||||
|
||||
if self._value_template is not None:
|
||||
|
|
|
@ -28,7 +28,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
|||
from homeassistant.util import dt as dt_util, slugify
|
||||
|
||||
from .const import CONF_COMMAND_TIMEOUT, LOGGER
|
||||
from .utils import call_shell_with_timeout, check_output_or_log
|
||||
from .utils import async_call_shell_with_timeout, async_check_output_or_log
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=15)
|
||||
|
||||
|
@ -114,11 +114,11 @@ class CommandCover(ManualTriggerEntity, CoverEntity):
|
|||
),
|
||||
)
|
||||
|
||||
def _move_cover(self, command: str) -> bool:
|
||||
async def _async_move_cover(self, command: str) -> bool:
|
||||
"""Execute the actual commands."""
|
||||
LOGGER.info("Running command: %s", command)
|
||||
|
||||
returncode = call_shell_with_timeout(command, self._timeout)
|
||||
returncode = await async_call_shell_with_timeout(command, self._timeout)
|
||||
success = returncode == 0
|
||||
|
||||
if not success:
|
||||
|
@ -143,11 +143,11 @@ class CommandCover(ManualTriggerEntity, CoverEntity):
|
|||
"""
|
||||
return self._state
|
||||
|
||||
def _query_state(self) -> str | None:
|
||||
async def _async_query_state(self) -> str | None:
|
||||
"""Query for the state."""
|
||||
if self._command_state:
|
||||
LOGGER.info("Running state value command: %s", self._command_state)
|
||||
return check_output_or_log(self._command_state, self._timeout)
|
||||
return await async_check_output_or_log(self._command_state, self._timeout)
|
||||
if TYPE_CHECKING:
|
||||
return None
|
||||
|
||||
|
@ -169,7 +169,7 @@ class CommandCover(ManualTriggerEntity, CoverEntity):
|
|||
async def _async_update(self) -> None:
|
||||
"""Update device state."""
|
||||
if self._command_state:
|
||||
payload = str(await self.hass.async_add_executor_job(self._query_state))
|
||||
payload = str(await self._async_query_state())
|
||||
if self._value_template:
|
||||
payload = self._value_template.async_render_with_possible_json_value(
|
||||
payload, None
|
||||
|
@ -189,15 +189,15 @@ class CommandCover(ManualTriggerEntity, CoverEntity):
|
|||
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Open the cover."""
|
||||
await self.hass.async_add_executor_job(self._move_cover, self._command_open)
|
||||
await self._async_move_cover(self._command_open)
|
||||
await self._update_entity_state()
|
||||
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close the cover."""
|
||||
await self.hass.async_add_executor_job(self._move_cover, self._command_close)
|
||||
await self._async_move_cover(self._command_close)
|
||||
await self._update_entity_state()
|
||||
|
||||
async def async_stop_cover(self, **kwargs: Any) -> None:
|
||||
"""Stop the cover."""
|
||||
await self.hass.async_add_executor_job(self._move_cover, self._command_stop)
|
||||
await self._async_move_cover(self._command_stop)
|
||||
await self._update_entity_state()
|
||||
|
|
|
@ -33,7 +33,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
|||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import CONF_COMMAND_TIMEOUT, LOGGER
|
||||
from .utils import check_output_or_log
|
||||
from .utils import async_check_output_or_log
|
||||
|
||||
CONF_JSON_ATTRIBUTES = "json_attributes"
|
||||
|
||||
|
@ -138,6 +138,7 @@ class CommandSensor(ManualTriggerSensorEntity):
|
|||
"""Update the state of the entity."""
|
||||
if self._process_updates is None:
|
||||
self._process_updates = asyncio.Lock()
|
||||
|
||||
if self._process_updates.locked():
|
||||
LOGGER.warning(
|
||||
"Updating Command Line Sensor %s took longer than the scheduled update interval %s",
|
||||
|
@ -151,7 +152,7 @@ class CommandSensor(ManualTriggerSensorEntity):
|
|||
|
||||
async def _async_update(self) -> None:
|
||||
"""Get the latest data and updates the state."""
|
||||
await self.hass.async_add_executor_job(self.data.update)
|
||||
await self.data.async_update()
|
||||
value = self.data.value
|
||||
|
||||
if self._json_attributes:
|
||||
|
@ -216,7 +217,7 @@ class CommandSensorData:
|
|||
self.command = command
|
||||
self.timeout = command_timeout
|
||||
|
||||
def update(self) -> None:
|
||||
async def async_update(self) -> None:
|
||||
"""Get the latest data with a shell command."""
|
||||
command = self.command
|
||||
|
||||
|
@ -231,7 +232,7 @@ class CommandSensorData:
|
|||
if args_compiled:
|
||||
try:
|
||||
args_to_render = {"arguments": args}
|
||||
rendered_args = args_compiled.render(args_to_render)
|
||||
rendered_args = args_compiled.async_render(args_to_render)
|
||||
except TemplateError as ex:
|
||||
LOGGER.exception("Error rendering command template: %s", ex)
|
||||
return
|
||||
|
@ -246,4 +247,4 @@ class CommandSensorData:
|
|||
command = f"{prog} {rendered_args}"
|
||||
|
||||
LOGGER.debug("Running command: %s", command)
|
||||
self.value = check_output_or_log(command, self.timeout)
|
||||
self.value = await async_check_output_or_log(command, self.timeout)
|
||||
|
|
|
@ -28,7 +28,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
|||
from homeassistant.util import dt as dt_util, slugify
|
||||
|
||||
from .const import CONF_COMMAND_TIMEOUT, LOGGER
|
||||
from .utils import call_shell_with_timeout, check_output_or_log
|
||||
from .utils import async_call_shell_with_timeout, async_check_output_or_log
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
|
@ -121,28 +121,26 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
|
|||
"""Execute the actual commands."""
|
||||
LOGGER.info("Running command: %s", command)
|
||||
|
||||
success = (
|
||||
await self.hass.async_add_executor_job(
|
||||
call_shell_with_timeout, command, self._timeout
|
||||
)
|
||||
== 0
|
||||
)
|
||||
success = await async_call_shell_with_timeout(command, self._timeout) == 0
|
||||
|
||||
if not success:
|
||||
LOGGER.error("Command failed: %s", command)
|
||||
|
||||
return success
|
||||
|
||||
def _query_state_value(self, command: str) -> str | None:
|
||||
async def _async_query_state_value(self, command: str) -> str | None:
|
||||
"""Execute state command for return value."""
|
||||
LOGGER.info("Running state value command: %s", command)
|
||||
return check_output_or_log(command, self._timeout)
|
||||
return await async_check_output_or_log(command, self._timeout)
|
||||
|
||||
def _query_state_code(self, command: str) -> bool:
|
||||
async def _async_query_state_code(self, command: str) -> bool:
|
||||
"""Execute state command for return code."""
|
||||
LOGGER.info("Running state code command: %s", command)
|
||||
return (
|
||||
call_shell_with_timeout(command, self._timeout, log_return_code=False) == 0
|
||||
await async_call_shell_with_timeout(
|
||||
command, self._timeout, log_return_code=False
|
||||
)
|
||||
== 0
|
||||
)
|
||||
|
||||
@property
|
||||
|
@ -150,12 +148,12 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
|
|||
"""Return true if we do optimistic updates."""
|
||||
return self._command_state is None
|
||||
|
||||
def _query_state(self) -> str | int | None:
|
||||
async def _async_query_state(self) -> str | int | None:
|
||||
"""Query for state."""
|
||||
if self._command_state:
|
||||
if self._value_template:
|
||||
return self._query_state_value(self._command_state)
|
||||
return self._query_state_code(self._command_state)
|
||||
return await self._async_query_state_value(self._command_state)
|
||||
return await self._async_query_state_code(self._command_state)
|
||||
if TYPE_CHECKING:
|
||||
return None
|
||||
|
||||
|
@ -177,7 +175,7 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
|
|||
async def _async_update(self) -> None:
|
||||
"""Update device state."""
|
||||
if self._command_state:
|
||||
payload = str(await self.hass.async_add_executor_job(self._query_state))
|
||||
payload = str(await self._async_query_state())
|
||||
value = None
|
||||
if self._value_template:
|
||||
value = self._value_template.async_render_with_possible_json_value(
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
"""The command_line component utils."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_EXEC_FAILED_CODE = 127
|
||||
|
||||
|
||||
def call_shell_with_timeout(
|
||||
async def async_call_shell_with_timeout(
|
||||
command: str, timeout: int, *, log_return_code: bool = True
|
||||
) -> int:
|
||||
"""Run a shell command with a timeout.
|
||||
|
@ -17,46 +18,45 @@ def call_shell_with_timeout(
|
|||
"""
|
||||
try:
|
||||
_LOGGER.debug("Running command: %s", command)
|
||||
subprocess.check_output(
|
||||
proc = await asyncio.create_subprocess_shell( # noqa: S602 # shell by design
|
||||
command,
|
||||
shell=True, # noqa: S602 # shell by design
|
||||
timeout=timeout,
|
||||
close_fds=False, # required for posix_spawn
|
||||
)
|
||||
return 0
|
||||
except subprocess.CalledProcessError as proc_exception:
|
||||
if log_return_code:
|
||||
async with asyncio.timeout(timeout):
|
||||
await proc.communicate()
|
||||
return_code = proc.returncode
|
||||
if return_code == _EXEC_FAILED_CODE:
|
||||
_LOGGER.error("Error trying to exec command: %s", command)
|
||||
elif log_return_code and return_code != 0:
|
||||
_LOGGER.error(
|
||||
"Command failed (with return code %s): %s",
|
||||
proc_exception.returncode,
|
||||
proc.returncode,
|
||||
command,
|
||||
)
|
||||
return proc_exception.returncode
|
||||
except subprocess.TimeoutExpired:
|
||||
return return_code or 0
|
||||
except TimeoutError:
|
||||
_LOGGER.error("Timeout for command: %s", command)
|
||||
return -1
|
||||
except subprocess.SubprocessError:
|
||||
_LOGGER.error("Error trying to exec command: %s", command)
|
||||
return -1
|
||||
|
||||
|
||||
def check_output_or_log(command: str, timeout: int) -> str | None:
|
||||
async def async_check_output_or_log(command: str, timeout: int) -> str | None:
|
||||
"""Run a shell command with a timeout and return the output."""
|
||||
try:
|
||||
return_value = subprocess.check_output(
|
||||
proc = await asyncio.create_subprocess_shell( # noqa: S602 # shell by design
|
||||
command,
|
||||
shell=True, # noqa: S602 # shell by design
|
||||
timeout=timeout,
|
||||
close_fds=False, # required for posix_spawn
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
)
|
||||
return return_value.strip().decode("utf-8")
|
||||
except subprocess.CalledProcessError as err:
|
||||
_LOGGER.error(
|
||||
"Command failed (with return code %s): %s", err.returncode, command
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
async with asyncio.timeout(timeout):
|
||||
stdout, _ = await proc.communicate()
|
||||
|
||||
if proc.returncode != 0:
|
||||
_LOGGER.error(
|
||||
"Command failed (with return code %s): %s", proc.returncode, command
|
||||
)
|
||||
else:
|
||||
return stdout.strip().decode("utf-8")
|
||||
except TimeoutError:
|
||||
_LOGGER.error("Timeout for command: %s", command)
|
||||
except subprocess.SubprocessError:
|
||||
_LOGGER.error("Error trying to exec command: %s", command)
|
||||
|
||||
return None
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue