mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
Cleanup command_line (#90268)
* Cleanup command_line * Fix ipv6 resolver * Fix fix * Fix tests * Align states
This commit is contained in:
parent
2ceb24e5d0
commit
96698813ef
@ -1,62 +1 @@
|
|||||||
"""The command_line component."""
|
"""The command_line component."""
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def call_shell_with_timeout(
|
|
||||||
command: str, timeout: int, *, log_return_code: bool = True
|
|
||||||
) -> int:
|
|
||||||
"""Run a shell command with a timeout.
|
|
||||||
|
|
||||||
If log_return_code is set to False, it will not print an error if a non-zero
|
|
||||||
return code is returned.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
_LOGGER.debug("Running command: %s", command)
|
|
||||||
subprocess.check_output(
|
|
||||||
command,
|
|
||||||
shell=True, # nosec # shell by design
|
|
||||||
timeout=timeout,
|
|
||||||
close_fds=False, # required for posix_spawn
|
|
||||||
)
|
|
||||||
return 0
|
|
||||||
except subprocess.CalledProcessError as proc_exception:
|
|
||||||
if log_return_code:
|
|
||||||
_LOGGER.error(
|
|
||||||
"Command failed (with return code %s): %s",
|
|
||||||
proc_exception.returncode,
|
|
||||||
command,
|
|
||||||
)
|
|
||||||
return proc_exception.returncode
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
_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:
|
|
||||||
"""Run a shell command with a timeout and return the output."""
|
|
||||||
try:
|
|
||||||
return_value = subprocess.check_output(
|
|
||||||
command,
|
|
||||||
shell=True, # nosec # shell by design
|
|
||||||
timeout=timeout,
|
|
||||||
close_fds=False, # required for posix_spawn
|
|
||||||
)
|
|
||||||
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:
|
|
||||||
_LOGGER.error("Timeout for command: %s", command)
|
|
||||||
except subprocess.SubprocessError:
|
|
||||||
_LOGGER.error("Error trying to exec command: %s", command)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
@ -25,10 +25,6 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.reload import async_setup_reload_service
|
from homeassistant.helpers.reload import async_setup_reload_service
|
||||||
from homeassistant.helpers.template import Template
|
from homeassistant.helpers.template import Template
|
||||||
from homeassistant.helpers.template_entity import (
|
|
||||||
TEMPLATE_ENTITY_BASE_SCHEMA,
|
|
||||||
TemplateEntity,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, PLATFORMS
|
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, PLATFORMS
|
||||||
@ -65,10 +61,6 @@ async def async_setup_platform(
|
|||||||
|
|
||||||
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
|
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
|
||||||
|
|
||||||
binary_sensor_config = vol.Schema(
|
|
||||||
TEMPLATE_ENTITY_BASE_SCHEMA.schema, extra=vol.REMOVE_EXTRA
|
|
||||||
)(config)
|
|
||||||
|
|
||||||
name: str = config.get(CONF_NAME, DEFAULT_NAME)
|
name: str = config.get(CONF_NAME, DEFAULT_NAME)
|
||||||
command: str = config[CONF_COMMAND]
|
command: str = config[CONF_COMMAND]
|
||||||
payload_off: str = config[CONF_PAYLOAD_OFF]
|
payload_off: str = config[CONF_PAYLOAD_OFF]
|
||||||
@ -84,8 +76,6 @@ async def async_setup_platform(
|
|||||||
async_add_entities(
|
async_add_entities(
|
||||||
[
|
[
|
||||||
CommandBinarySensor(
|
CommandBinarySensor(
|
||||||
hass,
|
|
||||||
binary_sensor_config,
|
|
||||||
data,
|
data,
|
||||||
name,
|
name,
|
||||||
device_class,
|
device_class,
|
||||||
@ -99,13 +89,11 @@ async def async_setup_platform(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CommandBinarySensor(TemplateEntity, BinarySensorEntity):
|
class CommandBinarySensor(BinarySensorEntity):
|
||||||
"""Representation of a command line binary sensor."""
|
"""Representation of a command line binary sensor."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
|
||||||
config: ConfigType,
|
|
||||||
data: CommandSensorData,
|
data: CommandSensorData,
|
||||||
name: str,
|
name: str,
|
||||||
device_class: BinarySensorDeviceClass | None,
|
device_class: BinarySensorDeviceClass | None,
|
||||||
@ -115,19 +103,14 @@ class CommandBinarySensor(TemplateEntity, BinarySensorEntity):
|
|||||||
unique_id: str | None,
|
unique_id: str | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the Command line binary sensor."""
|
"""Initialize the Command line binary sensor."""
|
||||||
TemplateEntity.__init__(
|
|
||||||
self,
|
|
||||||
hass,
|
|
||||||
config=config,
|
|
||||||
fallback_name=name,
|
|
||||||
unique_id=unique_id,
|
|
||||||
)
|
|
||||||
self.data = data
|
self.data = data
|
||||||
|
self._attr_name = name
|
||||||
self._attr_device_class = device_class
|
self._attr_device_class = device_class
|
||||||
self._attr_is_on = None
|
self._attr_is_on = None
|
||||||
self._payload_on = payload_on
|
self._payload_on = payload_on
|
||||||
self._payload_off = payload_off
|
self._payload_off = payload_off
|
||||||
self._value_template = value_template
|
self._value_template = value_template
|
||||||
|
self._attr_unique_id = unique_id
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Get the latest data and updates the state."""
|
"""Get the latest data and updates the state."""
|
||||||
@ -135,9 +118,10 @@ class CommandBinarySensor(TemplateEntity, BinarySensorEntity):
|
|||||||
value = self.data.value
|
value = self.data.value
|
||||||
|
|
||||||
if self._value_template is not None:
|
if self._value_template is not None:
|
||||||
value = await self.hass.async_add_executor_job(
|
value = self._value_template.async_render_with_possible_json_value(
|
||||||
self._value_template.render_with_possible_json_value, value, False
|
value, None
|
||||||
)
|
)
|
||||||
|
self._attr_is_on = None
|
||||||
if value == self._payload_on:
|
if value == self._payload_on:
|
||||||
self._attr_is_on = True
|
self._attr_is_on = True
|
||||||
elif value == self._payload_off:
|
elif value == self._payload_off:
|
||||||
|
@ -22,14 +22,10 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.reload import async_setup_reload_service
|
from homeassistant.helpers.reload import async_setup_reload_service
|
||||||
from homeassistant.helpers.template import Template
|
from homeassistant.helpers.template import Template
|
||||||
from homeassistant.helpers.template_entity import (
|
|
||||||
TEMPLATE_ENTITY_BASE_SCHEMA,
|
|
||||||
TemplateEntity,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
from . import call_shell_with_timeout, check_output_or_log
|
|
||||||
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, PLATFORMS
|
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, PLATFORMS
|
||||||
|
from .utils import call_shell_with_timeout, check_output_or_log
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -69,14 +65,8 @@ async def async_setup_platform(
|
|||||||
if value_template is not None:
|
if value_template is not None:
|
||||||
value_template.hass = hass
|
value_template.hass = hass
|
||||||
|
|
||||||
cover_config = vol.Schema(
|
|
||||||
TEMPLATE_ENTITY_BASE_SCHEMA.schema, extra=vol.REMOVE_EXTRA
|
|
||||||
)(device_config)
|
|
||||||
|
|
||||||
covers.append(
|
covers.append(
|
||||||
CommandCover(
|
CommandCover(
|
||||||
hass,
|
|
||||||
cover_config,
|
|
||||||
device_config.get(CONF_FRIENDLY_NAME, device_name),
|
device_config.get(CONF_FRIENDLY_NAME, device_name),
|
||||||
device_config[CONF_COMMAND_OPEN],
|
device_config[CONF_COMMAND_OPEN],
|
||||||
device_config[CONF_COMMAND_CLOSE],
|
device_config[CONF_COMMAND_CLOSE],
|
||||||
@ -95,13 +85,11 @@ async def async_setup_platform(
|
|||||||
async_add_entities(covers)
|
async_add_entities(covers)
|
||||||
|
|
||||||
|
|
||||||
class CommandCover(TemplateEntity, CoverEntity):
|
class CommandCover(CoverEntity):
|
||||||
"""Representation a command line cover."""
|
"""Representation a command line cover."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
|
||||||
config: ConfigType,
|
|
||||||
name: str,
|
name: str,
|
||||||
command_open: str,
|
command_open: str,
|
||||||
command_close: str,
|
command_close: str,
|
||||||
@ -112,13 +100,7 @@ class CommandCover(TemplateEntity, CoverEntity):
|
|||||||
unique_id: str | None,
|
unique_id: str | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the cover."""
|
"""Initialize the cover."""
|
||||||
TemplateEntity.__init__(
|
self._attr_name = name
|
||||||
self,
|
|
||||||
hass,
|
|
||||||
config=config,
|
|
||||||
fallback_name=name,
|
|
||||||
unique_id=unique_id,
|
|
||||||
)
|
|
||||||
self._state: int | None = None
|
self._state: int | None = None
|
||||||
self._command_open = command_open
|
self._command_open = command_open
|
||||||
self._command_close = command_close
|
self._command_close = command_close
|
||||||
@ -126,6 +108,7 @@ class CommandCover(TemplateEntity, CoverEntity):
|
|||||||
self._command_state = command_state
|
self._command_state = command_state
|
||||||
self._value_template = value_template
|
self._value_template = value_template
|
||||||
self._timeout = timeout
|
self._timeout = timeout
|
||||||
|
self._attr_unique_id = unique_id
|
||||||
self._attr_should_poll = bool(command_state)
|
self._attr_should_poll = bool(command_state)
|
||||||
|
|
||||||
def _move_cover(self, command: str) -> bool:
|
def _move_cover(self, command: str) -> bool:
|
||||||
@ -170,9 +153,11 @@ class CommandCover(TemplateEntity, CoverEntity):
|
|||||||
if self._command_state:
|
if self._command_state:
|
||||||
payload = str(await self.hass.async_add_executor_job(self._query_state))
|
payload = str(await self.hass.async_add_executor_job(self._query_state))
|
||||||
if self._value_template:
|
if self._value_template:
|
||||||
payload = await self.hass.async_add_executor_job(
|
payload = self._value_template.async_render_with_possible_json_value(
|
||||||
self._value_template.render_with_possible_json_value, payload
|
payload, None
|
||||||
)
|
)
|
||||||
|
self._state = None
|
||||||
|
if payload:
|
||||||
self._state = int(payload)
|
self._state = int(payload)
|
||||||
|
|
||||||
def open_cover(self, **kwargs: Any) -> None:
|
def open_cover(self, **kwargs: Any) -> None:
|
||||||
|
@ -22,7 +22,6 @@ from homeassistant.const import (
|
|||||||
CONF_UNIQUE_ID,
|
CONF_UNIQUE_ID,
|
||||||
CONF_UNIT_OF_MEASUREMENT,
|
CONF_UNIT_OF_MEASUREMENT,
|
||||||
CONF_VALUE_TEMPLATE,
|
CONF_VALUE_TEMPLATE,
|
||||||
STATE_UNKNOWN,
|
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import TemplateError
|
from homeassistant.exceptions import TemplateError
|
||||||
@ -30,14 +29,10 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.reload import async_setup_reload_service
|
from homeassistant.helpers.reload import async_setup_reload_service
|
||||||
from homeassistant.helpers.template import Template
|
from homeassistant.helpers.template import Template
|
||||||
from homeassistant.helpers.template_entity import (
|
|
||||||
TEMPLATE_SENSOR_BASE_SCHEMA,
|
|
||||||
TemplateSensor,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
from . import check_output_or_log
|
|
||||||
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, PLATFORMS
|
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, PLATFORMS
|
||||||
|
from .utils import check_output_or_log
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -72,10 +67,6 @@ async def async_setup_platform(
|
|||||||
|
|
||||||
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
|
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
|
||||||
|
|
||||||
sensor_config = vol.Schema(
|
|
||||||
TEMPLATE_SENSOR_BASE_SCHEMA.schema, extra=vol.REMOVE_EXTRA
|
|
||||||
)(config)
|
|
||||||
|
|
||||||
name: str = config[CONF_NAME]
|
name: str = config[CONF_NAME]
|
||||||
command: str = config[CONF_COMMAND]
|
command: str = config[CONF_COMMAND]
|
||||||
unit: str | None = config.get(CONF_UNIT_OF_MEASUREMENT)
|
unit: str | None = config.get(CONF_UNIT_OF_MEASUREMENT)
|
||||||
@ -90,8 +81,6 @@ async def async_setup_platform(
|
|||||||
async_add_entities(
|
async_add_entities(
|
||||||
[
|
[
|
||||||
CommandSensor(
|
CommandSensor(
|
||||||
hass,
|
|
||||||
sensor_config,
|
|
||||||
data,
|
data,
|
||||||
name,
|
name,
|
||||||
unit,
|
unit,
|
||||||
@ -104,13 +93,11 @@ async def async_setup_platform(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CommandSensor(TemplateSensor, SensorEntity):
|
class CommandSensor(SensorEntity):
|
||||||
"""Representation of a sensor that is using shell commands."""
|
"""Representation of a sensor that is using shell commands."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
|
||||||
config: ConfigType,
|
|
||||||
data: CommandSensorData,
|
data: CommandSensorData,
|
||||||
name: str,
|
name: str,
|
||||||
unit_of_measurement: str | None,
|
unit_of_measurement: str | None,
|
||||||
@ -119,18 +106,14 @@ class CommandSensor(TemplateSensor, SensorEntity):
|
|||||||
unique_id: str | None,
|
unique_id: str | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
TemplateSensor.__init__(
|
self._attr_name = name
|
||||||
self,
|
|
||||||
hass,
|
|
||||||
config=config,
|
|
||||||
fallback_name=name,
|
|
||||||
unique_id=unique_id,
|
|
||||||
)
|
|
||||||
self.data = data
|
self.data = data
|
||||||
self._attr_extra_state_attributes = {}
|
self._attr_extra_state_attributes = {}
|
||||||
self._json_attributes = json_attributes
|
self._json_attributes = json_attributes
|
||||||
self._attr_native_value = None
|
self._attr_native_value = None
|
||||||
self._value_template = value_template
|
self._value_template = value_template
|
||||||
|
self._attr_native_unit_of_measurement = unit_of_measurement
|
||||||
|
self._attr_unique_id = unique_id
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Get the latest data and updates the state."""
|
"""Get the latest data and updates the state."""
|
||||||
@ -155,13 +138,12 @@ class CommandSensor(TemplateSensor, SensorEntity):
|
|||||||
else:
|
else:
|
||||||
_LOGGER.warning("Empty reply found when expecting JSON data")
|
_LOGGER.warning("Empty reply found when expecting JSON data")
|
||||||
|
|
||||||
if value is None:
|
|
||||||
value = STATE_UNKNOWN
|
|
||||||
elif self._value_template is not None:
|
elif self._value_template is not None:
|
||||||
self._attr_native_value = await self.hass.async_add_executor_job(
|
self._attr_native_value = (
|
||||||
self._value_template.render_with_possible_json_value,
|
self._value_template.async_render_with_possible_json_value(
|
||||||
value,
|
value,
|
||||||
STATE_UNKNOWN,
|
None,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self._attr_native_value = value
|
self._attr_native_value = value
|
||||||
|
@ -24,12 +24,12 @@ from homeassistant.const import (
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.reload import setup_reload_service
|
from homeassistant.helpers.reload import async_setup_reload_service
|
||||||
from homeassistant.helpers.template import Template
|
from homeassistant.helpers.template import Template
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
from . import call_shell_with_timeout, check_output_or_log
|
|
||||||
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, PLATFORMS
|
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, PLATFORMS
|
||||||
|
from .utils import call_shell_with_timeout, check_output_or_log
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -51,15 +51,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(
|
async def async_setup_platform(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config: ConfigType,
|
config: ConfigType,
|
||||||
add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
discovery_info: DiscoveryInfoType | None = None,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Find and return switches controlled by shell commands."""
|
"""Find and return switches controlled by shell commands."""
|
||||||
|
|
||||||
setup_reload_service(hass, DOMAIN, PLATFORMS)
|
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
|
||||||
|
|
||||||
devices: dict[str, Any] = config.get(CONF_SWITCHES, {})
|
devices: dict[str, Any] = config.get(CONF_SWITCHES, {})
|
||||||
switches = []
|
switches = []
|
||||||
@ -92,7 +92,7 @@ def setup_platform(
|
|||||||
_LOGGER.error("No switches added")
|
_LOGGER.error("No switches added")
|
||||||
return
|
return
|
||||||
|
|
||||||
add_entities(switches)
|
async_add_entities(switches)
|
||||||
|
|
||||||
|
|
||||||
class CommandSwitch(SwitchEntity):
|
class CommandSwitch(SwitchEntity):
|
||||||
@ -123,11 +123,16 @@ class CommandSwitch(SwitchEntity):
|
|||||||
self._attr_unique_id = unique_id
|
self._attr_unique_id = unique_id
|
||||||
self._attr_should_poll = bool(command_state)
|
self._attr_should_poll = bool(command_state)
|
||||||
|
|
||||||
def _switch(self, command: str) -> bool:
|
async def _switch(self, command: str) -> bool:
|
||||||
"""Execute the actual commands."""
|
"""Execute the actual commands."""
|
||||||
_LOGGER.info("Running command: %s", command)
|
_LOGGER.info("Running command: %s", command)
|
||||||
|
|
||||||
success = call_shell_with_timeout(command, self._timeout) == 0
|
success = (
|
||||||
|
await self.hass.async_add_executor_job(
|
||||||
|
call_shell_with_timeout, command, self._timeout
|
||||||
|
)
|
||||||
|
== 0
|
||||||
|
)
|
||||||
|
|
||||||
if not success:
|
if not success:
|
||||||
_LOGGER.error("Command failed: %s", command)
|
_LOGGER.error("Command failed: %s", command)
|
||||||
@ -160,26 +165,30 @@ class CommandSwitch(SwitchEntity):
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Update device state."""
|
"""Update device state."""
|
||||||
if self._command_state:
|
if self._command_state:
|
||||||
payload = str(self._query_state())
|
payload = str(await self.hass.async_add_executor_job(self._query_state))
|
||||||
if self._icon_template:
|
if self._icon_template:
|
||||||
self._attr_icon = self._icon_template.render_with_possible_json_value(
|
self._attr_icon = (
|
||||||
payload
|
self._icon_template.async_render_with_possible_json_value(payload)
|
||||||
)
|
)
|
||||||
if self._value_template:
|
if self._value_template:
|
||||||
payload = self._value_template.render_with_possible_json_value(payload)
|
payload = self._value_template.async_render_with_possible_json_value(
|
||||||
|
payload, None
|
||||||
|
)
|
||||||
|
self._attr_is_on = None
|
||||||
|
if payload:
|
||||||
self._attr_is_on = payload.lower() == "true"
|
self._attr_is_on = payload.lower() == "true"
|
||||||
|
|
||||||
def turn_on(self, **kwargs: Any) -> None:
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
"""Turn the device on."""
|
"""Turn the device on."""
|
||||||
if self._switch(self._command_on) and not self._command_state:
|
if await self._switch(self._command_on) and not self._command_state:
|
||||||
self._attr_is_on = True
|
self._attr_is_on = True
|
||||||
self.schedule_update_ha_state()
|
self.async_schedule_update_ha_state()
|
||||||
|
|
||||||
def turn_off(self, **kwargs: Any) -> None:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Turn the device off."""
|
"""Turn the device off."""
|
||||||
if self._switch(self._command_off) and not self._command_state:
|
if await self._switch(self._command_off) and not self._command_state:
|
||||||
self._attr_is_on = False
|
self._attr_is_on = False
|
||||||
self.schedule_update_ha_state()
|
self.async_schedule_update_ha_state()
|
||||||
|
62
homeassistant/components/command_line/utils.py
Normal file
62
homeassistant/components/command_line/utils.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
"""The command_line component utils."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def call_shell_with_timeout(
|
||||||
|
command: str, timeout: int, *, log_return_code: bool = True
|
||||||
|
) -> int:
|
||||||
|
"""Run a shell command with a timeout.
|
||||||
|
|
||||||
|
If log_return_code is set to False, it will not print an error if a non-zero
|
||||||
|
return code is returned.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
_LOGGER.debug("Running command: %s", command)
|
||||||
|
subprocess.check_output(
|
||||||
|
command,
|
||||||
|
shell=True, # nosec # shell by design
|
||||||
|
timeout=timeout,
|
||||||
|
close_fds=False, # required for posix_spawn
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
except subprocess.CalledProcessError as proc_exception:
|
||||||
|
if log_return_code:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Command failed (with return code %s): %s",
|
||||||
|
proc_exception.returncode,
|
||||||
|
command,
|
||||||
|
)
|
||||||
|
return proc_exception.returncode
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
_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:
|
||||||
|
"""Run a shell command with a timeout and return the output."""
|
||||||
|
try:
|
||||||
|
return_value = subprocess.check_output(
|
||||||
|
command,
|
||||||
|
shell=True, # nosec # shell by design
|
||||||
|
timeout=timeout,
|
||||||
|
close_fds=False, # required for posix_spawn
|
||||||
|
)
|
||||||
|
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:
|
||||||
|
_LOGGER.error("Timeout for command: %s", command)
|
||||||
|
except subprocess.SubprocessError:
|
||||||
|
_LOGGER.error("Error trying to exec command: %s", command)
|
||||||
|
|
||||||
|
return None
|
@ -42,7 +42,7 @@ async def test_no_covers(caplog: pytest.LogCaptureFixture, hass: HomeAssistant)
|
|||||||
"""Test that the cover does not polls when there's no state command."""
|
"""Test that the cover does not polls when there's no state command."""
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.command_line.subprocess.check_output",
|
"homeassistant.components.command_line.utils.subprocess.check_output",
|
||||||
return_value=b"50\n",
|
return_value=b"50\n",
|
||||||
):
|
):
|
||||||
await setup_test_entity(hass, {})
|
await setup_test_entity(hass, {})
|
||||||
@ -53,7 +53,7 @@ async def test_no_poll_when_cover_has_no_command_state(hass: HomeAssistant) -> N
|
|||||||
"""Test that the cover does not polls when there's no state command."""
|
"""Test that the cover does not polls when there's no state command."""
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.command_line.subprocess.check_output",
|
"homeassistant.components.command_line.utils.subprocess.check_output",
|
||||||
return_value=b"50\n",
|
return_value=b"50\n",
|
||||||
) as check_output:
|
) as check_output:
|
||||||
await setup_test_entity(hass, {"test": {}})
|
await setup_test_entity(hass, {"test": {}})
|
||||||
@ -66,7 +66,7 @@ async def test_poll_when_cover_has_command_state(hass: HomeAssistant) -> None:
|
|||||||
"""Test that the cover polls when there's a state command."""
|
"""Test that the cover polls when there's a state command."""
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.command_line.subprocess.check_output",
|
"homeassistant.components.command_line.utils.subprocess.check_output",
|
||||||
return_value=b"50\n",
|
return_value=b"50\n",
|
||||||
) as check_output:
|
) as check_output:
|
||||||
await setup_test_entity(hass, {"test": {"command_state": "echo state"}})
|
await setup_test_entity(hass, {"test": {"command_state": "echo state"}})
|
||||||
|
@ -88,7 +88,7 @@ async def test_template_render_with_quote(hass: HomeAssistant) -> None:
|
|||||||
"""Ensure command with templates and quotes get rendered properly."""
|
"""Ensure command with templates and quotes get rendered properly."""
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.command_line.subprocess.check_output",
|
"homeassistant.components.command_line.utils.subprocess.check_output",
|
||||||
return_value=b"Works\n",
|
return_value=b"Works\n",
|
||||||
) as check_output:
|
) as check_output:
|
||||||
await setup_test_entities(
|
await setup_test_entities(
|
||||||
|
@ -323,7 +323,7 @@ async def test_switch_command_state_code_exceptions(
|
|||||||
"""Test that switch state code exceptions are handled correctly."""
|
"""Test that switch state code exceptions are handled correctly."""
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.command_line.subprocess.check_output",
|
"homeassistant.components.command_line.utils.subprocess.check_output",
|
||||||
side_effect=[
|
side_effect=[
|
||||||
subprocess.TimeoutExpired("cmd", 10),
|
subprocess.TimeoutExpired("cmd", 10),
|
||||||
subprocess.SubprocessError(),
|
subprocess.SubprocessError(),
|
||||||
@ -356,7 +356,7 @@ async def test_switch_command_state_value_exceptions(
|
|||||||
"""Test that switch state value exceptions are handled correctly."""
|
"""Test that switch state value exceptions are handled correctly."""
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.command_line.subprocess.check_output",
|
"homeassistant.components.command_line.utils.subprocess.check_output",
|
||||||
side_effect=[
|
side_effect=[
|
||||||
subprocess.TimeoutExpired("cmd", 10),
|
subprocess.TimeoutExpired("cmd", 10),
|
||||||
subprocess.SubprocessError(),
|
subprocess.SubprocessError(),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user