diff --git a/homeassistant/components/command_line/__init__.py b/homeassistant/components/command_line/__init__.py index c0713d0780b..fe0640d3efa 100644 --- a/homeassistant/components/command_line/__init__.py +++ b/homeassistant/components/command_line/__init__.py @@ -1,62 +1 @@ """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 diff --git a/homeassistant/components/command_line/binary_sensor.py b/homeassistant/components/command_line/binary_sensor.py index 2e1ddb7a962..0c2edb8f191 100644 --- a/homeassistant/components/command_line/binary_sensor.py +++ b/homeassistant/components/command_line/binary_sensor.py @@ -25,10 +25,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.reload import async_setup_reload_service 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 .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) - binary_sensor_config = vol.Schema( - TEMPLATE_ENTITY_BASE_SCHEMA.schema, extra=vol.REMOVE_EXTRA - )(config) - name: str = config.get(CONF_NAME, DEFAULT_NAME) command: str = config[CONF_COMMAND] payload_off: str = config[CONF_PAYLOAD_OFF] @@ -84,8 +76,6 @@ async def async_setup_platform( async_add_entities( [ CommandBinarySensor( - hass, - binary_sensor_config, data, name, 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.""" def __init__( self, - hass: HomeAssistant, - config: ConfigType, data: CommandSensorData, name: str, device_class: BinarySensorDeviceClass | None, @@ -115,19 +103,14 @@ class CommandBinarySensor(TemplateEntity, BinarySensorEntity): unique_id: str | None, ) -> None: """Initialize the Command line binary sensor.""" - TemplateEntity.__init__( - self, - hass, - config=config, - fallback_name=name, - unique_id=unique_id, - ) self.data = data + self._attr_name = name self._attr_device_class = device_class self._attr_is_on = None self._payload_on = payload_on self._payload_off = payload_off self._value_template = value_template + self._attr_unique_id = unique_id async def async_update(self) -> None: """Get the latest data and updates the state.""" @@ -135,9 +118,10 @@ class CommandBinarySensor(TemplateEntity, BinarySensorEntity): value = self.data.value if self._value_template is not None: - value = await self.hass.async_add_executor_job( - self._value_template.render_with_possible_json_value, value, False + value = self._value_template.async_render_with_possible_json_value( + value, None ) + self._attr_is_on = None if value == self._payload_on: self._attr_is_on = True elif value == self._payload_off: diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index 53773ae4e91..e477affc854 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -22,14 +22,10 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.reload import async_setup_reload_service 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 . import call_shell_with_timeout, check_output_or_log 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__) @@ -69,14 +65,8 @@ async def async_setup_platform( if value_template is not None: value_template.hass = hass - cover_config = vol.Schema( - TEMPLATE_ENTITY_BASE_SCHEMA.schema, extra=vol.REMOVE_EXTRA - )(device_config) - covers.append( CommandCover( - hass, - cover_config, device_config.get(CONF_FRIENDLY_NAME, device_name), device_config[CONF_COMMAND_OPEN], device_config[CONF_COMMAND_CLOSE], @@ -95,13 +85,11 @@ async def async_setup_platform( async_add_entities(covers) -class CommandCover(TemplateEntity, CoverEntity): +class CommandCover(CoverEntity): """Representation a command line cover.""" def __init__( self, - hass: HomeAssistant, - config: ConfigType, name: str, command_open: str, command_close: str, @@ -112,13 +100,7 @@ class CommandCover(TemplateEntity, CoverEntity): unique_id: str | None, ) -> None: """Initialize the cover.""" - TemplateEntity.__init__( - self, - hass, - config=config, - fallback_name=name, - unique_id=unique_id, - ) + self._attr_name = name self._state: int | None = None self._command_open = command_open self._command_close = command_close @@ -126,6 +108,7 @@ class CommandCover(TemplateEntity, CoverEntity): self._command_state = command_state self._value_template = value_template self._timeout = timeout + self._attr_unique_id = unique_id self._attr_should_poll = bool(command_state) def _move_cover(self, command: str) -> bool: @@ -170,10 +153,12 @@ class CommandCover(TemplateEntity, CoverEntity): if self._command_state: payload = str(await self.hass.async_add_executor_job(self._query_state)) if self._value_template: - payload = await self.hass.async_add_executor_job( - self._value_template.render_with_possible_json_value, payload + payload = self._value_template.async_render_with_possible_json_value( + payload, None ) - self._state = int(payload) + self._state = None + if payload: + self._state = int(payload) def open_cover(self, **kwargs: Any) -> None: """Open the cover.""" diff --git a/homeassistant/components/command_line/sensor.py b/homeassistant/components/command_line/sensor.py index 24224c12cac..f459e415661 100644 --- a/homeassistant/components/command_line/sensor.py +++ b/homeassistant/components/command_line/sensor.py @@ -22,7 +22,6 @@ from homeassistant.const import ( CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, - STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant 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.reload import async_setup_reload_service 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 . import check_output_or_log from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, PLATFORMS +from .utils import check_output_or_log _LOGGER = logging.getLogger(__name__) @@ -72,10 +67,6 @@ async def async_setup_platform( 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] command: str = config[CONF_COMMAND] unit: str | None = config.get(CONF_UNIT_OF_MEASUREMENT) @@ -90,8 +81,6 @@ async def async_setup_platform( async_add_entities( [ CommandSensor( - hass, - sensor_config, data, name, 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.""" def __init__( self, - hass: HomeAssistant, - config: ConfigType, data: CommandSensorData, name: str, unit_of_measurement: str | None, @@ -119,18 +106,14 @@ class CommandSensor(TemplateSensor, SensorEntity): unique_id: str | None, ) -> None: """Initialize the sensor.""" - TemplateSensor.__init__( - self, - hass, - config=config, - fallback_name=name, - unique_id=unique_id, - ) + self._attr_name = name self.data = data self._attr_extra_state_attributes = {} self._json_attributes = json_attributes self._attr_native_value = None 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: """Get the latest data and updates the state.""" @@ -155,13 +138,12 @@ class CommandSensor(TemplateSensor, SensorEntity): else: _LOGGER.warning("Empty reply found when expecting JSON data") - if value is None: - value = STATE_UNKNOWN elif self._value_template is not None: - self._attr_native_value = await self.hass.async_add_executor_job( - self._value_template.render_with_possible_json_value, - value, - STATE_UNKNOWN, + self._attr_native_value = ( + self._value_template.async_render_with_possible_json_value( + value, + None, + ) ) else: self._attr_native_value = value diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index 7142f14e82d..3c344891fba 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -24,12 +24,12 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv 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.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 .utils import call_shell_with_timeout, check_output_or_log _LOGGER = logging.getLogger(__name__) @@ -51,15 +51,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """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, {}) switches = [] @@ -92,7 +92,7 @@ def setup_platform( _LOGGER.error("No switches added") return - add_entities(switches) + async_add_entities(switches) class CommandSwitch(SwitchEntity): @@ -123,11 +123,16 @@ class CommandSwitch(SwitchEntity): self._attr_unique_id = unique_id self._attr_should_poll = bool(command_state) - def _switch(self, command: str) -> bool: + async def _switch(self, command: str) -> bool: """Execute the actual commands.""" _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: _LOGGER.error("Command failed: %s", command) @@ -160,26 +165,30 @@ class CommandSwitch(SwitchEntity): if TYPE_CHECKING: return None - def update(self) -> None: + async def async_update(self) -> None: """Update device 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: - self._attr_icon = self._icon_template.render_with_possible_json_value( - payload + self._attr_icon = ( + self._icon_template.async_render_with_possible_json_value(payload) ) if self._value_template: - payload = self._value_template.render_with_possible_json_value(payload) - self._attr_is_on = payload.lower() == "true" + 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" - def turn_on(self, **kwargs: Any) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """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.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.""" - 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.schedule_update_ha_state() + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/command_line/utils.py b/homeassistant/components/command_line/utils.py new file mode 100644 index 00000000000..2d42732190e --- /dev/null +++ b/homeassistant/components/command_line/utils.py @@ -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 diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py index bfb74832f90..a650bd6c4fb 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -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.""" with patch( - "homeassistant.components.command_line.subprocess.check_output", + "homeassistant.components.command_line.utils.subprocess.check_output", return_value=b"50\n", ): 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.""" with patch( - "homeassistant.components.command_line.subprocess.check_output", + "homeassistant.components.command_line.utils.subprocess.check_output", return_value=b"50\n", ) as check_output: 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.""" with patch( - "homeassistant.components.command_line.subprocess.check_output", + "homeassistant.components.command_line.utils.subprocess.check_output", return_value=b"50\n", ) as check_output: await setup_test_entity(hass, {"test": {"command_state": "echo state"}}) diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index 5aab14225f1..4643891691f 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -88,7 +88,7 @@ async def test_template_render_with_quote(hass: HomeAssistant) -> None: """Ensure command with templates and quotes get rendered properly.""" with patch( - "homeassistant.components.command_line.subprocess.check_output", + "homeassistant.components.command_line.utils.subprocess.check_output", return_value=b"Works\n", ) as check_output: await setup_test_entities( diff --git a/tests/components/command_line/test_switch.py b/tests/components/command_line/test_switch.py index ac1ae357123..bc8eadcb22f 100644 --- a/tests/components/command_line/test_switch.py +++ b/tests/components/command_line/test_switch.py @@ -323,7 +323,7 @@ async def test_switch_command_state_code_exceptions( """Test that switch state code exceptions are handled correctly.""" with patch( - "homeassistant.components.command_line.subprocess.check_output", + "homeassistant.components.command_line.utils.subprocess.check_output", side_effect=[ subprocess.TimeoutExpired("cmd", 10), subprocess.SubprocessError(), @@ -356,7 +356,7 @@ async def test_switch_command_state_value_exceptions( """Test that switch state value exceptions are handled correctly.""" with patch( - "homeassistant.components.command_line.subprocess.check_output", + "homeassistant.components.command_line.utils.subprocess.check_output", side_effect=[ subprocess.TimeoutExpired("cmd", 10), subprocess.SubprocessError(),