diff --git a/homeassistant/components/command_line/__init__.py b/homeassistant/components/command_line/__init__.py index ba4292b5a65..701391ab389 100644 --- a/homeassistant/components/command_line/__init__.py +++ b/homeassistant/components/command_line/__init__.py @@ -55,6 +55,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import async_get_platforms from homeassistant.helpers.reload import async_integration_yaml_config from homeassistant.helpers.service import async_register_admin_service +from homeassistant.helpers.trigger_template_entity import CONF_AVAILABILITY from homeassistant.helpers.typing import ConfigType from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN @@ -90,6 +91,7 @@ BINARY_SENSOR_SCHEMA = vol.Schema( vol.Optional( CONF_SCAN_INTERVAL, default=BINARY_SENSOR_DEFAULT_SCAN_INTERVAL ): vol.All(cv.time_period, cv.positive_timedelta), + vol.Optional(CONF_AVAILABILITY): cv.template, } ) COVER_SCHEMA = vol.Schema( @@ -105,6 +107,7 @@ COVER_SCHEMA = vol.Schema( vol.Optional(CONF_SCAN_INTERVAL, default=COVER_DEFAULT_SCAN_INTERVAL): vol.All( cv.time_period, cv.positive_timedelta ), + vol.Optional(CONF_AVAILABILITY): cv.template, } ) NOTIFY_SCHEMA = vol.Schema( @@ -129,6 +132,7 @@ SENSOR_SCHEMA = vol.Schema( vol.Optional(CONF_SCAN_INTERVAL, default=SENSOR_DEFAULT_SCAN_INTERVAL): vol.All( cv.time_period, cv.positive_timedelta ), + vol.Optional(CONF_AVAILABILITY): cv.template, } ) SWITCH_SCHEMA = vol.Schema( @@ -144,6 +148,7 @@ SWITCH_SCHEMA = vol.Schema( vol.Optional(CONF_SCAN_INTERVAL, default=SWITCH_DEFAULT_SCAN_INTERVAL): vol.All( cv.time_period, cv.positive_timedelta ), + vol.Optional(CONF_AVAILABILITY): cv.template, } ) COMBINED_SCHEMA = vol.Schema( diff --git a/homeassistant/components/command_line/binary_sensor.py b/homeassistant/components/command_line/binary_sensor.py index 31259ddf909..20b538fc4d7 100644 --- a/homeassistant/components/command_line/binary_sensor.py +++ b/homeassistant/components/command_line/binary_sensor.py @@ -24,7 +24,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.template import Template -from homeassistant.helpers.trigger_template_entity import ManualTriggerEntity +from homeassistant.helpers.trigger_template_entity import ( + CONF_AVAILABILITY, + ManualTriggerEntity, +) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util @@ -63,6 +66,7 @@ async def async_setup_platform( scan_interval: timedelta = binary_sensor_config.get( CONF_SCAN_INTERVAL, SCAN_INTERVAL ) + availability: Template | None = binary_sensor_config.get(CONF_AVAILABILITY) if value_template is not None: value_template.hass = hass data = CommandSensorData(hass, command, command_timeout) @@ -72,6 +76,7 @@ async def async_setup_platform( CONF_NAME: Template(name, hass), CONF_DEVICE_CLASS: device_class, CONF_ICON: icon, + CONF_AVAILABILITY: availability, } async_add_entities( diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index 93c007297ea..845de352d73 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -20,7 +20,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.template import Template -from homeassistant.helpers.trigger_template_entity import ManualTriggerEntity +from homeassistant.helpers.trigger_template_entity import ( + CONF_AVAILABILITY, + ManualTriggerEntity, +) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util, slugify @@ -50,6 +53,7 @@ async def async_setup_platform( trigger_entity_config = { CONF_UNIQUE_ID: device_config.get(CONF_UNIQUE_ID), CONF_NAME: Template(device_config.get(CONF_NAME, device_name), hass), + CONF_AVAILABILITY: device_config.get(CONF_AVAILABILITY), } covers.append( diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index 0af6163312c..efeded194ce 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -20,7 +20,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.template import Template -from homeassistant.helpers.trigger_template_entity import ManualTriggerEntity +from homeassistant.helpers.trigger_template_entity import ( + CONF_AVAILABILITY, + ManualTriggerEntity, +) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util, slugify @@ -48,6 +51,7 @@ async def async_setup_platform( CONF_UNIQUE_ID: device_config.get(CONF_UNIQUE_ID), CONF_NAME: Template(device_config.get(CONF_NAME, object_id), hass), CONF_ICON: device_config.get(CONF_ICON), + CONF_AVAILABILITY: device_config.get(CONF_AVAILABILITY), } value_template: Template | None = device_config.get(CONF_VALUE_TEMPLATE) diff --git a/tests/components/command_line/test_binary_sensor.py b/tests/components/command_line/test_binary_sensor.py index eaa7061551a..7975660fda3 100644 --- a/tests/components/command_line/test_binary_sensor.py +++ b/tests/components/command_line/test_binary_sensor.py @@ -6,6 +6,7 @@ from datetime import timedelta from typing import Any from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant import setup @@ -15,7 +16,7 @@ from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, SERVICE_UPDATE_ENTITY, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util @@ -289,3 +290,53 @@ async def test_updating_manually( ) await hass.async_block_till_done() assert called + + +@pytest.mark.parametrize( + "get_config", + [ + { + "command_line": [ + { + "binary_sensor": { + "name": "Test", + "command": "echo 10", + "payload_on": "1.0", + "payload_off": "0", + "value_template": "{{ value | multiply(0.1) }}", + "availability": '{{ states("sensor.input1")=="on" }}', + } + } + ] + } + ], +) +async def test_availability( + hass: HomeAssistant, + load_yaml_integration: None, + freezer: FrozenDateTimeFactory, +) -> None: + """Test availability.""" + + hass.states.async_set("sensor.input1", "on") + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + entity_state = hass.states.get("binary_sensor.test") + assert entity_state + assert entity_state.state == STATE_ON + + hass.states.async_set("sensor.input1", "off") + await hass.async_block_till_done() + with patch( + "homeassistant.components.command_line.utils.subprocess.check_output", + return_value=b"0", + ): + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + entity_state = hass.states.get("binary_sensor.test") + assert entity_state + assert entity_state.state == STATE_UNAVAILABLE diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py index e6e428388f4..901fc39eb34 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -7,6 +7,7 @@ import os import tempfile from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant import setup @@ -22,6 +23,8 @@ from homeassistant.const import ( SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER, SERVICE_STOP_COVER, + STATE_OPEN, + STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -340,3 +343,50 @@ async def test_updating_manually( ) await hass.async_block_till_done() assert called + + +@pytest.mark.parametrize( + "get_config", + [ + { + "command_line": [ + { + "cover": { + "command_state": "echo 10", + "name": "Test", + "availability": '{{ states("sensor.input1")=="on" }}', + }, + } + ] + } + ], +) +async def test_availability( + hass: HomeAssistant, + load_yaml_integration: None, + freezer: FrozenDateTimeFactory, +) -> None: + """Test availability.""" + + hass.states.async_set("sensor.input1", "on") + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + entity_state = hass.states.get("cover.test") + assert entity_state + assert entity_state.state == STATE_OPEN + + hass.states.async_set("sensor.input1", "off") + await hass.async_block_till_done() + with patch( + "homeassistant.components.command_line.utils.subprocess.check_output", + return_value=b"50\n", + ): + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + entity_state = hass.states.get("cover.test") + assert entity_state + assert entity_state.state == STATE_UNAVAILABLE diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index 9f28b8cc6d0..64227116cfe 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -7,6 +7,7 @@ import subprocess from typing import Any from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant import setup @@ -16,7 +17,7 @@ from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, SERVICE_UPDATE_ENTITY, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util @@ -708,3 +709,52 @@ async def test_template_not_error_when_data_is_none( "Template variable error: 'None' has no attribute 'split' when rendering" not in caplog.text ) + + +@pytest.mark.parametrize( + "get_config", + [ + { + "command_line": [ + { + "sensor": { + "name": "Test", + "command": "echo January 17, 2022", + "device_class": "date", + "value_template": "{{ strptime(value, '%B %d, %Y').strftime('%Y-%m-%d') }}", + "availability": '{{ states("sensor.input1")=="on" }}', + } + } + ] + } + ], +) +async def test_availability( + hass: HomeAssistant, + load_yaml_integration: None, + freezer: FrozenDateTimeFactory, +) -> None: + """Test availability.""" + + hass.states.async_set("sensor.input1", "on") + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + entity_state = hass.states.get("sensor.test") + assert entity_state + assert entity_state.state == "2022-01-17" + + hass.states.async_set("sensor.input1", "off") + await hass.async_block_till_done() + with patch( + "homeassistant.components.command_line.utils.subprocess.check_output", + return_value=b"January 17, 2022", + ): + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + entity_state = hass.states.get("sensor.test") + assert entity_state + assert entity_state.state == STATE_UNAVAILABLE diff --git a/tests/components/command_line/test_switch.py b/tests/components/command_line/test_switch.py index f1f4096fa91..47d9184f4f9 100644 --- a/tests/components/command_line/test_switch.py +++ b/tests/components/command_line/test_switch.py @@ -9,6 +9,7 @@ import subprocess import tempfile from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant import setup @@ -25,6 +26,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_OFF, STATE_ON, + STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -710,3 +712,52 @@ async def test_updating_manually( ) await hass.async_block_till_done() assert called + + +@pytest.mark.parametrize( + "get_config", + [ + { + "command_line": [ + { + "switch": { + "command_state": "echo 1", + "command_on": "echo 2", + "command_off": "echo 3", + "name": "Test", + "availability": '{{ states("sensor.input1")=="on" }}', + }, + } + ] + } + ], +) +async def test_availability( + hass: HomeAssistant, + load_yaml_integration: None, + freezer: FrozenDateTimeFactory, +) -> None: + """Test availability.""" + + hass.states.async_set("sensor.input1", "on") + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + entity_state = hass.states.get("switch.test") + assert entity_state + assert entity_state.state == STATE_ON + + hass.states.async_set("sensor.input1", "off") + await hass.async_block_till_done() + with patch( + "homeassistant.components.command_line.utils.subprocess.check_output", + return_value=b"50\n", + ): + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + entity_state = hass.states.get("switch.test") + assert entity_state + assert entity_state.state == STATE_UNAVAILABLE