diff --git a/CODEOWNERS b/CODEOWNERS index f01a1173b20..763c47840d3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -213,6 +213,8 @@ build.json @home-assistant/supervisor /tests/components/color_extractor/ @GenericStudent /homeassistant/components/comfoconnect/ @michaelarnauts /tests/components/comfoconnect/ @michaelarnauts +/homeassistant/components/command_line/ @gjohansson-ST +/tests/components/command_line/ @gjohansson-ST /homeassistant/components/compensation/ @Petro31 /tests/components/compensation/ @Petro31 /homeassistant/components/config/ @home-assistant/core diff --git a/homeassistant/components/command_line/__init__.py b/homeassistant/components/command_line/__init__.py index fe0640d3efa..c34b35bb62f 100644 --- a/homeassistant/components/command_line/__init__.py +++ b/homeassistant/components/command_line/__init__.py @@ -1 +1,177 @@ """The command_line component.""" +from __future__ import annotations + +import asyncio +from collections.abc import Coroutine +import logging +from typing import Any + +import voluptuous as vol + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, + DOMAIN as BINARY_SENSOR_DOMAIN, +) +from homeassistant.components.cover import DOMAIN as COVER_DOMAIN +from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN +from homeassistant.components.sensor import ( + CONF_STATE_CLASS, + DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA, + DOMAIN as SENSOR_DOMAIN, + STATE_CLASSES_SCHEMA as SENSOR_STATE_CLASSES_SCHEMA, +) +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + CONF_COMMAND, + CONF_COMMAND_CLOSE, + CONF_COMMAND_OFF, + CONF_COMMAND_ON, + CONF_COMMAND_OPEN, + CONF_COMMAND_STATE, + CONF_COMMAND_STOP, + CONF_DEVICE_CLASS, + CONF_ICON, + CONF_NAME, + CONF_PAYLOAD_OFF, + CONF_PAYLOAD_ON, + CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, + CONF_VALUE_TEMPLATE, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.reload import async_setup_reload_service +from homeassistant.helpers.typing import ConfigType + +from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN + +BINARY_SENSOR_DEFAULT_NAME = "Binary Command Sensor" +DEFAULT_PAYLOAD_ON = "ON" +DEFAULT_PAYLOAD_OFF = "OFF" +CONF_JSON_ATTRIBUTES = "json_attributes" +SENSOR_DEFAULT_NAME = "Command Sensor" +CONF_NOTIFIERS = "notifiers" + +PLATFORM_MAPPING = { + BINARY_SENSOR_DOMAIN: Platform.BINARY_SENSOR, + COVER_DOMAIN: Platform.COVER, + NOTIFY_DOMAIN: Platform.NOTIFY, + SENSOR_DOMAIN: Platform.SENSOR, + SWITCH_DOMAIN: Platform.SWITCH, +} + +_LOGGER = logging.getLogger(__name__) + +BINARY_SENSOR_SCHEMA = vol.Schema( + { + vol.Required(CONF_COMMAND): cv.string, + vol.Optional(CONF_NAME, default=BINARY_SENSOR_DEFAULT_NAME): cv.string, + vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, + vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, + vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + vol.Optional(CONF_UNIQUE_ID): cv.string, + } +) +COVER_SCHEMA = vol.Schema( + { + vol.Optional(CONF_COMMAND_CLOSE, default="true"): cv.string, + vol.Optional(CONF_COMMAND_OPEN, default="true"): cv.string, + vol.Optional(CONF_COMMAND_STATE): cv.string, + vol.Optional(CONF_COMMAND_STOP, default="true"): cv.string, + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + vol.Optional(CONF_UNIQUE_ID): cv.string, + } +) +NOTIFY_SCHEMA = vol.Schema( + { + vol.Required(CONF_COMMAND): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + } +) +SENSOR_SCHEMA = vol.Schema( + { + vol.Required(CONF_COMMAND): cv.string, + vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + vol.Optional(CONF_JSON_ATTRIBUTES): cv.ensure_list_csv, + vol.Optional(CONF_NAME, default=SENSOR_DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA, + } +) +SWITCH_SCHEMA = vol.Schema( + { + vol.Optional(CONF_COMMAND_OFF, default="true"): cv.string, + vol.Optional(CONF_COMMAND_ON, default="true"): cv.string, + vol.Optional(CONF_COMMAND_STATE): cv.string, + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_ICON): cv.template, + vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + vol.Optional(CONF_UNIQUE_ID): cv.string, + } +) +COMBINED_SCHEMA = vol.Schema( + { + vol.Optional(BINARY_SENSOR_DOMAIN): BINARY_SENSOR_SCHEMA, + vol.Optional(COVER_DOMAIN): COVER_SCHEMA, + vol.Optional(NOTIFY_DOMAIN): NOTIFY_SCHEMA, + vol.Optional(SENSOR_DOMAIN): SENSOR_SCHEMA, + vol.Optional(SWITCH_DOMAIN): SWITCH_SCHEMA, + } +) +CONFIG_SCHEMA = vol.Schema( + { + vol.Optional(DOMAIN): vol.All( + cv.ensure_list, + [COMBINED_SCHEMA], + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up Command Line from yaml config.""" + command_line_config: list[dict[str, dict[str, Any]]] = config.get(DOMAIN, {}) + if not command_line_config: + return True + + _LOGGER.debug("Full config loaded: %s", command_line_config) + + load_coroutines: list[Coroutine[Any, Any, None]] = [] + platforms: list[Platform] = [] + for platform_config in command_line_config: + for platform, _config in platform_config.items(): + platforms.append(PLATFORM_MAPPING[platform]) + _LOGGER.debug( + "Loading config %s for platform %s", + platform_config, + PLATFORM_MAPPING[platform], + ) + load_coroutines.append( + discovery.async_load_platform( + hass, + PLATFORM_MAPPING[platform], + DOMAIN, + _config, + config, + ) + ) + + await async_setup_reload_service(hass, DOMAIN, platforms) + + if load_coroutines: + _LOGGER.debug("Loading platforms: %s", platforms) + await asyncio.gather(*load_coroutines) + + return True diff --git a/homeassistant/components/command_line/binary_sensor.py b/homeassistant/components/command_line/binary_sensor.py index 0c2edb8f191..18b3cf71eb0 100644 --- a/homeassistant/components/command_line/binary_sensor.py +++ b/homeassistant/components/command_line/binary_sensor.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, + DOMAIN as BINARY_SENSOR_DOMAIN, PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, @@ -23,11 +24,11 @@ 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 async_setup_reload_service +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.template import Template 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 from .sensor import CommandSensorData DEFAULT_NAME = "Binary Command Sensor" @@ -59,16 +60,30 @@ async def async_setup_platform( ) -> None: """Set up the Command line Binary Sensor.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + if binary_sensor_config := config: + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml_binary_sensor", + breaks_in_ha_version="2023.8.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_platform_yaml", + translation_placeholders={"platform": BINARY_SENSOR_DOMAIN}, + ) + if discovery_info: + binary_sensor_config = discovery_info - name: str = config.get(CONF_NAME, DEFAULT_NAME) - command: str = config[CONF_COMMAND] - payload_off: str = config[CONF_PAYLOAD_OFF] - payload_on: str = config[CONF_PAYLOAD_ON] - device_class: BinarySensorDeviceClass | None = config.get(CONF_DEVICE_CLASS) - value_template: Template | None = config.get(CONF_VALUE_TEMPLATE) - command_timeout: int = config[CONF_COMMAND_TIMEOUT] - unique_id: str | None = config.get(CONF_UNIQUE_ID) + name: str = binary_sensor_config.get(CONF_NAME, DEFAULT_NAME) + command: str = binary_sensor_config[CONF_COMMAND] + payload_off: str = binary_sensor_config[CONF_PAYLOAD_OFF] + payload_on: str = binary_sensor_config[CONF_PAYLOAD_ON] + device_class: BinarySensorDeviceClass | None = binary_sensor_config.get( + CONF_DEVICE_CLASS + ) + value_template: Template | None = binary_sensor_config.get(CONF_VALUE_TEMPLATE) + command_timeout: int = binary_sensor_config[CONF_COMMAND_TIMEOUT] + unique_id: str | None = binary_sensor_config.get(CONF_UNIQUE_ID) if value_template is not None: value_template.hass = hass data = CommandSensorData(hass, command, command_timeout) diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index e477affc854..4503ceb8e56 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -6,7 +6,11 @@ from typing import TYPE_CHECKING, Any import voluptuous as vol -from homeassistant.components.cover import PLATFORM_SCHEMA, CoverEntity +from homeassistant.components.cover import ( + DOMAIN as COVER_DOMAIN, + PLATFORM_SCHEMA, + CoverEntity, +) from homeassistant.const import ( CONF_COMMAND_CLOSE, CONF_COMMAND_OPEN, @@ -14,17 +18,19 @@ from homeassistant.const import ( CONF_COMMAND_STOP, CONF_COVERS, CONF_FRIENDLY_NAME, + CONF_NAME, CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) from homeassistant.core import HomeAssistant 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.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.util import slugify -from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, PLATFORMS +from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN from .utils import call_shell_with_timeout, check_output_or_log _LOGGER = logging.getLogger(__name__) @@ -55,19 +61,35 @@ async def async_setup_platform( ) -> None: """Set up cover controlled by shell commands.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - - devices: dict[str, Any] = config.get(CONF_COVERS, {}) covers = [] + if discovery_info: + entities: dict[str, Any] = {slugify(discovery_info[CONF_NAME]): discovery_info} + else: + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml_cover", + breaks_in_ha_version="2023.8.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_platform_yaml", + translation_placeholders={"platform": COVER_DOMAIN}, + ) + entities = config.get(CONF_COVERS, {}) - for device_name, device_config in devices.items(): + for device_name, device_config in entities.items(): value_template: Template | None = device_config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = hass + if name := device_config.get( + CONF_FRIENDLY_NAME + ): # Backward compatibility. Can be removed after deprecation + device_config[CONF_NAME] = name + covers.append( CommandCover( - device_config.get(CONF_FRIENDLY_NAME, device_name), + device_config.get(CONF_NAME, device_name), device_config[CONF_COMMAND_OPEN], device_config[CONF_COMMAND_CLOSE], device_config[CONF_COMMAND_STOP], diff --git a/homeassistant/components/command_line/manifest.json b/homeassistant/components/command_line/manifest.json index 998c02aad9e..e99234bed1b 100644 --- a/homeassistant/components/command_line/manifest.json +++ b/homeassistant/components/command_line/manifest.json @@ -1,7 +1,7 @@ { "domain": "command_line", "name": "Command Line", - "codeowners": [], + "codeowners": ["@gjohansson-ST"], "documentation": "https://www.home-assistant.io/integrations/command_line", "iot_class": "local_polling" } diff --git a/homeassistant/components/command_line/notify.py b/homeassistant/components/command_line/notify.py index 412456ff6e5..2922b8caae3 100644 --- a/homeassistant/components/command_line/notify.py +++ b/homeassistant/components/command_line/notify.py @@ -7,14 +7,19 @@ from typing import Any import voluptuous as vol -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService +from homeassistant.components.notify import ( + DOMAIN as NOTIFY_DOMAIN, + PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import CONF_COMMAND, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.process import kill_subprocess -from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT +from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -33,8 +38,21 @@ def get_service( discovery_info: DiscoveryInfoType | None = None, ) -> CommandLineNotificationService: """Get the Command Line notification service.""" - command: str = config[CONF_COMMAND] - timeout: int = config[CONF_COMMAND_TIMEOUT] + if notify_config := config: + create_issue( + hass, + DOMAIN, + "deprecated_yaml_notify", + breaks_in_ha_version="2023.8.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_platform_yaml", + translation_placeholders={"platform": NOTIFY_DOMAIN}, + ) + if discovery_info: + notify_config = discovery_info + command: str = notify_config[CONF_COMMAND] + timeout: int = notify_config[CONF_COMMAND_TIMEOUT] return CommandLineNotificationService(command, timeout) diff --git a/homeassistant/components/command_line/sensor.py b/homeassistant/components/command_line/sensor.py index b6a2b8d83fa..1689b136f2f 100644 --- a/homeassistant/components/command_line/sensor.py +++ b/homeassistant/components/command_line/sensor.py @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( CONF_STATE_CLASS, DEVICE_CLASSES_SCHEMA, + DOMAIN as SENSOR_DOMAIN, PLATFORM_SCHEMA, STATE_CLASSES_SCHEMA, SensorEntity, @@ -27,11 +28,11 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import TemplateError 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.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.template import Template 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 from .utils import check_output_or_log _LOGGER = logging.getLogger(__name__) @@ -64,18 +65,29 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Command Sensor.""" + if sensor_config := config: + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml_sensor", + breaks_in_ha_version="2023.8.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_platform_yaml", + translation_placeholders={"platform": SENSOR_DOMAIN}, + ) + if discovery_info: + sensor_config = discovery_info - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - - name: str = config[CONF_NAME] - command: str = config[CONF_COMMAND] - unit: str | None = config.get(CONF_UNIT_OF_MEASUREMENT) - value_template: Template | None = config.get(CONF_VALUE_TEMPLATE) - command_timeout: int = config[CONF_COMMAND_TIMEOUT] - unique_id: str | None = config.get(CONF_UNIQUE_ID) + name: str = sensor_config[CONF_NAME] + command: str = sensor_config[CONF_COMMAND] + unit: str | None = sensor_config.get(CONF_UNIT_OF_MEASUREMENT) + value_template: Template | None = sensor_config.get(CONF_VALUE_TEMPLATE) + command_timeout: int = sensor_config[CONF_COMMAND_TIMEOUT] + unique_id: str | None = sensor_config.get(CONF_UNIQUE_ID) if value_template is not None: value_template.hass = hass - json_attributes: list[str] | None = config.get(CONF_JSON_ATTRIBUTES) + json_attributes: list[str] | None = sensor_config.get(CONF_JSON_ATTRIBUTES) data = CommandSensorData(hass, command, command_timeout) async_add_entities( diff --git a/homeassistant/components/command_line/strings.json b/homeassistant/components/command_line/strings.json new file mode 100644 index 00000000000..5c1e7d11630 --- /dev/null +++ b/homeassistant/components/command_line/strings.json @@ -0,0 +1,8 @@ +{ + "issues": { + "deprecated_platform_yaml": { + "title": "Command Line YAML configuration has been deprecated", + "description": "Configuring Command Line `{platform}` using YAML has been deprecated.\n\nConsult the documentation to move your YAML configuration to integration key and restart Home Assistant to resolve this issue." + } + } +} diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index bfb45f5b5c4..7936bacd432 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Any import voluptuous as vol from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, ENTITY_ID_FORMAT, PLATFORM_SCHEMA, SwitchEntity, @@ -26,12 +27,13 @@ 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 async_setup_reload_service +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.template import Template from homeassistant.helpers.template_entity import ManualTriggerEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.util import slugify -from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, PLATFORMS +from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN from .utils import call_shell_with_timeout, check_output_or_log _LOGGER = logging.getLogger(__name__) @@ -62,16 +64,38 @@ async def async_setup_platform( ) -> None: """Find and return switches controlled by shell commands.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + if discovery_info: + entities: dict[str, Any] = {slugify(discovery_info[CONF_NAME]): discovery_info} + else: + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml_switch", + breaks_in_ha_version="2023.8.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_platform_yaml", + translation_placeholders={"platform": SWITCH_DOMAIN}, + ) + entities = config.get(CONF_SWITCHES, {}) - devices: dict[str, Any] = config.get(CONF_SWITCHES, {}) switches = [] - for object_id, device_config in devices.items(): + for object_id, device_config in entities.items(): + if name := device_config.get( + CONF_FRIENDLY_NAME + ): # Backward compatibility. Can be removed after deprecation + device_config[CONF_NAME] = name + + if icon := device_config.get( + CONF_ICON_TEMPLATE + ): # Backward compatibility. Can be removed after deprecation + device_config[CONF_ICON] = icon + trigger_entity_config = { CONF_UNIQUE_ID: device_config.get(CONF_UNIQUE_ID), - CONF_NAME: Template(device_config.get(CONF_FRIENDLY_NAME, object_id), hass), - CONF_ICON: device_config.get(CONF_ICON_TEMPLATE), + CONF_NAME: Template(device_config.get(CONF_NAME, object_id), hass), + CONF_ICON: device_config.get(CONF_ICON), } value_template: Template | None = device_config.get(CONF_VALUE_TEMPLATE) diff --git a/tests/components/command_line/conftest.py b/tests/components/command_line/conftest.py new file mode 100644 index 00000000000..5dc4152f8f2 --- /dev/null +++ b/tests/components/command_line/conftest.py @@ -0,0 +1,72 @@ +"""Fixtures for command_line.""" + +from typing import Any + +import pytest + +from homeassistant import setup +from homeassistant.components.command_line.const import DOMAIN +from homeassistant.core import HomeAssistant + + +@pytest.fixture(name="get_config") +async def get_config_to_integration_load() -> dict[str, Any]: + """Return default minimal configuration. + + To override the config, tests can be marked with: + @pytest.mark.parametrize("get_config", [{...}]) + """ + + return { + "command_line": [ + { + "binary_sensor": { + "name": "Test", + "command": "echo 1", + "payload_on": "1", + "payload_off": "0", + "command_timeout": 15, + } + }, + { + "cover": { + "name": "Test", + "command_state": "echo 1", + "command_timeout": 15, + } + }, + { + "notify": { + "name": "Test", + "command": "echo 1", + "command_timeout": 15, + } + }, + { + "sensor": { + "name": "Test", + "command": "echo 5", + "unit_of_measurement": "in", + "command_timeout": 15, + } + }, + { + "switch": { + "name": "Test", + "command_state": "echo 1", + "command_timeout": 15, + } + }, + ] + } + + +@pytest.fixture(name="load_yaml_integration") +async def load_int(hass: HomeAssistant, get_config: dict[str, Any]) -> None: + """Set up the Command Line integration in Home Assistant.""" + await setup.async_setup_component( + hass, + DOMAIN, + get_config, + ) + await hass.async_block_till_done() diff --git a/tests/components/command_line/test_binary_sensor.py b/tests/components/command_line/test_binary_sensor.py index a6486b40040..6f79b6bdacf 100644 --- a/tests/components/command_line/test_binary_sensor.py +++ b/tests/components/command_line/test_binary_sensor.py @@ -6,32 +6,63 @@ from typing import Any import pytest from homeassistant import setup -from homeassistant.components.binary_sensor import DOMAIN +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.command_line.const import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +import homeassistant.helpers.issue_registry as ir -async def setup_test_entity(hass: HomeAssistant, config_dict: dict[str, Any]) -> None: - """Set up a test command line binary_sensor entity.""" +async def test_setup_platform_yaml(hass: HomeAssistant) -> None: + """Test sensor setup.""" assert await setup.async_setup_component( hass, - DOMAIN, - {DOMAIN: {"platform": "command_line", "name": "Test", **config_dict}}, + BINARY_SENSOR_DOMAIN, + { + BINARY_SENSOR_DOMAIN: { + "platform": "command_line", + "name": "Test", + "command": "echo 1", + "payload_on": "1", + "payload_off": "0", + } + }, ) await hass.async_block_till_done() + entity_state = hass.states.get("binary_sensor.test") + assert entity_state + assert entity_state.state == STATE_ON + assert entity_state.name == "Test" -async def test_setup(hass: HomeAssistant) -> None: - """Test sensor setup.""" - await setup_test_entity( - hass, + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue(DOMAIN, "deprecated_yaml_binary_sensor") + assert issue.translation_key == "deprecated_platform_yaml" + + +@pytest.mark.parametrize( + "get_config", + [ { - "command": "echo 1", - "payload_on": "1", - "payload_off": "0", - }, - ) + "command_line": [ + { + "binary_sensor": { + "name": "Test", + "command": "echo 1", + "payload_on": "1", + "payload_off": "0", + "command_timeout": 15, + } + } + ] + } + ], +) +async def test_setup_integration_yaml( + hass: HomeAssistant, load_yaml_integration: None +) -> None: + """Test sensor setup.""" entity_state = hass.states.get("binary_sensor.test") assert entity_state @@ -39,67 +70,88 @@ async def test_setup(hass: HomeAssistant) -> None: assert entity_state.name == "Test" -async def test_template(hass: HomeAssistant) -> None: - """Test setting the state with a template.""" - - await setup_test_entity( - hass, +@pytest.mark.parametrize( + "get_config", + [ { - "command": "echo 10", - "payload_on": "1.0", - "payload_off": "0", - "value_template": "{{ value | multiply(0.1) }}", - }, - ) + "command_line": [ + { + "binary_sensor": { + "name": "Test", + "command": "echo 10", + "payload_on": "1.0", + "payload_off": "0", + "value_template": "{{ value | multiply(0.1) }}", + } + } + ] + } + ], +) +async def test_template(hass: HomeAssistant, load_yaml_integration: None) -> None: + """Test setting the state with a template.""" entity_state = hass.states.get("binary_sensor.test") assert entity_state assert entity_state.state == STATE_ON -async def test_sensor_off(hass: HomeAssistant) -> None: - """Test setting the state with a template.""" - await setup_test_entity( - hass, +@pytest.mark.parametrize( + "get_config", + [ { - "command": "echo 0", - "payload_on": "1", - "payload_off": "0", - }, - ) + "command_line": [ + { + "binary_sensor": { + "name": "Test", + "command": "echo 0", + "payload_on": "1", + "payload_off": "0", + } + } + ] + } + ], +) +async def test_sensor_off(hass: HomeAssistant, load_yaml_integration: None) -> None: + """Test setting the state with a template.""" + entity_state = hass.states.get("binary_sensor.test") assert entity_state assert entity_state.state == STATE_OFF -async def test_unique_id( - hass: HomeAssistant, entity_registry: er.EntityRegistry -) -> None: - """Test unique_id option and if it only creates one binary sensor per id.""" - assert await setup.async_setup_component( - hass, - DOMAIN, +@pytest.mark.parametrize( + "get_config", + [ { - DOMAIN: [ + "command_line": [ { - "platform": "command_line", - "unique_id": "unique", - "command": "echo 0", + "binary_sensor": { + "unique_id": "unique", + "command": "echo 0", + } }, { - "platform": "command_line", - "unique_id": "not-so-unique-anymore", - "command": "echo 1", + "binary_sensor": { + "unique_id": "not-so-unique-anymore", + "command": "echo 1", + } }, { - "platform": "command_line", - "unique_id": "not-so-unique-anymore", - "command": "echo 2", + "binary_sensor": { + "unique_id": "not-so-unique-anymore", + "command": "echo 2", + } }, ] - }, - ) - await hass.async_block_till_done() + } + ], +) +async def test_unique_id( + hass: HomeAssistant, entity_registry: er.EntityRegistry, load_yaml_integration: None +) -> None: + """Test unique_id option and if it only creates one binary sensor per id.""" assert len(hass.states.async_all()) == 2 @@ -112,14 +164,28 @@ async def test_unique_id( ) +@pytest.mark.parametrize( + "get_config", + [ + { + "command_line": [ + { + "binary_sensor": { + "command": "exit 33", + } + } + ] + } + ], +) async def test_return_code( - caplog: pytest.LogCaptureFixture, hass: HomeAssistant + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, get_config: dict[str, Any] ) -> None: """Test setting the state with a template.""" - await setup_test_entity( + await setup.async_setup_component( hass, - { - "command": "exit 33", - }, + DOMAIN, + get_config, ) + await hass.async_block_till_done() assert "return code 33" in caplog.text diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py index a650bd6c4fb..057e632c325 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -3,13 +3,13 @@ from __future__ import annotations import os import tempfile -from typing import Any from unittest.mock import patch import pytest from homeassistant import config as hass_config, setup -from homeassistant.components.cover import DOMAIN, SCAN_INTERVAL +from homeassistant.components.command_line import DOMAIN +from homeassistant.components.cover import DOMAIN as COVER_DOMAIN, SCAN_INTERVAL from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_CLOSE_COVER, @@ -19,35 +19,79 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +import homeassistant.helpers.issue_registry as ir import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed, get_fixture_path -async def setup_test_entity(hass: HomeAssistant, config_dict: dict[str, Any]) -> None: - """Set up a test command line notify service.""" - assert await setup.async_setup_component( - hass, - DOMAIN, - { - DOMAIN: [ - {"platform": "command_line", "covers": config_dict}, - ] - }, - ) - await hass.async_block_till_done() - - -async def test_no_covers(caplog: pytest.LogCaptureFixture, hass: HomeAssistant) -> None: +async def test_no_covers_platform_yaml( + caplog: pytest.LogCaptureFixture, hass: HomeAssistant +) -> None: """Test that the cover does not polls when there's no state command.""" with patch( "homeassistant.components.command_line.utils.subprocess.check_output", return_value=b"50\n", ): - await setup_test_entity(hass, {}) + assert await setup.async_setup_component( + hass, + COVER_DOMAIN, + { + COVER_DOMAIN: [ + {"platform": "command_line", "covers": {}}, + ] + }, + ) + await hass.async_block_till_done() assert "No covers added" in caplog.text + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue(DOMAIN, "deprecated_yaml_cover") + assert issue.translation_key == "deprecated_platform_yaml" + + +async def test_state_value_platform_yaml(hass: HomeAssistant) -> None: + """Test with state value.""" + with tempfile.TemporaryDirectory() as tempdirname: + path = os.path.join(tempdirname, "cover_status") + assert await setup.async_setup_component( + hass, + COVER_DOMAIN, + { + COVER_DOMAIN: [ + { + "platform": "command_line", + "covers": { + "test": { + "command_state": f"cat {path}", + "command_open": f"echo 1 > {path}", + "command_close": f"echo 1 > {path}", + "command_stop": f"echo 0 > {path}", + "value_template": "{{ value }}", + "friendly_name": "Test", + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + + entity_state = hass.states.get("cover.test") + assert entity_state + assert entity_state.state == "unknown" + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: "cover.test"}, + blocking=True, + ) + entity_state = hass.states.get("cover.test") + assert entity_state + assert entity_state.state == "open" + async def test_no_poll_when_cover_has_no_command_state(hass: HomeAssistant) -> None: """Test that the cover does not polls when there's no state command.""" @@ -56,20 +100,44 @@ async def test_no_poll_when_cover_has_no_command_state(hass: HomeAssistant) -> N "homeassistant.components.command_line.utils.subprocess.check_output", return_value=b"50\n", ) as check_output: - await setup_test_entity(hass, {"test": {}}) + assert await setup.async_setup_component( + hass, + COVER_DOMAIN, + { + COVER_DOMAIN: [ + {"platform": "command_line", "covers": {"test": {}}}, + ] + }, + ) async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() assert not check_output.called -async def test_poll_when_cover_has_command_state(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "get_config", + [ + { + "command_line": [ + { + "cover": { + "command_state": "echo state", + "name": "Test", + }, + } + ] + } + ], +) +async def test_poll_when_cover_has_command_state( + hass: HomeAssistant, load_yaml_integration: None +) -> None: """Test that the cover polls when there's a state command.""" with patch( "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"}}) async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() check_output.assert_called_once_with( @@ -84,57 +152,80 @@ async def test_state_value(hass: HomeAssistant) -> None: """Test with state value.""" with tempfile.TemporaryDirectory() as tempdirname: path = os.path.join(tempdirname, "cover_status") - await setup_test_entity( + await setup.async_setup_component( hass, + DOMAIN, { - "test": { - "command_state": f"cat {path}", - "command_open": f"echo 1 > {path}", - "command_close": f"echo 1 > {path}", - "command_stop": f"echo 0 > {path}", - "value_template": "{{ value }}", - } + "command_line": [ + { + "cover": { + "command_state": f"cat {path}", + "command_open": f"echo 1 > {path}", + "command_close": f"echo 1 > {path}", + "command_stop": f"echo 0 > {path}", + "value_template": "{{ value }}", + "name": "Test", + } + } + ] }, ) + await hass.async_block_till_done() entity_state = hass.states.get("cover.test") assert entity_state assert entity_state.state == "unknown" await hass.services.async_call( - DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: "cover.test"}, blocking=True + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: "cover.test"}, + blocking=True, ) entity_state = hass.states.get("cover.test") assert entity_state assert entity_state.state == "open" await hass.services.async_call( - DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: "cover.test"}, blocking=True + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.test"}, + blocking=True, ) entity_state = hass.states.get("cover.test") assert entity_state assert entity_state.state == "open" await hass.services.async_call( - DOMAIN, SERVICE_STOP_COVER, {ATTR_ENTITY_ID: "cover.test"}, blocking=True + COVER_DOMAIN, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: "cover.test"}, + blocking=True, ) entity_state = hass.states.get("cover.test") assert entity_state assert entity_state.state == "closed" -async def test_reload(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "get_config", + [ + { + "command_line": [ + { + "cover": { + "command_state": "echo open", + "value_template": "{{ value }}", + "name": "Test", + } + } + ] + } + ], +) +async def test_reload(hass: HomeAssistant, load_yaml_integration: None) -> None: """Verify we can reload command_line covers.""" - await setup_test_entity( - hass, - { - "test": { - "command_state": "echo open", - "value_template": "{{ value }}", - } - }, - ) entity_state = hass.states.get("cover.test") assert entity_state assert entity_state.state == "unknown" @@ -155,50 +246,73 @@ async def test_reload(hass: HomeAssistant) -> None: assert hass.states.get("cover.from_yaml") +@pytest.mark.parametrize( + "get_config", + [ + { + "command_line": [ + { + "cover": { + "command_open": "exit 1", + "name": "Test", + } + } + ] + } + ], +) async def test_move_cover_failure( - caplog: pytest.LogCaptureFixture, hass: HomeAssistant + caplog: pytest.LogCaptureFixture, hass: HomeAssistant, load_yaml_integration: None ) -> None: """Test command failure.""" - await setup_test_entity( - hass, - {"test": {"command_open": "exit 1"}}, - ) await hass.services.async_call( - DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: "cover.test"}, blocking=True + COVER_DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: "cover.test"}, blocking=True ) assert "Command failed" in caplog.text assert "return code 1" in caplog.text +@pytest.mark.parametrize( + "get_config", + [ + { + "command_line": [ + { + "cover": { + "command_open": "echo open", + "command_close": "echo close", + "command_stop": "echo stop", + "unique_id": "unique", + "name": "Test", + } + }, + { + "cover": { + "command_open": "echo open", + "command_close": "echo close", + "command_stop": "echo stop", + "unique_id": "not-so-unique-anymore", + "name": "Test2", + } + }, + { + "cover": { + "command_open": "echo open", + "command_close": "echo close", + "command_stop": "echo stop", + "unique_id": "not-so-unique-anymore", + "name": "Test3", + } + }, + ] + } + ], +) async def test_unique_id( - hass: HomeAssistant, entity_registry: er.EntityRegistry + hass: HomeAssistant, entity_registry: er.EntityRegistry, load_yaml_integration: None ) -> None: """Test unique_id option and if it only creates one cover per id.""" - await setup_test_entity( - hass, - { - "unique": { - "command_open": "echo open", - "command_close": "echo close", - "command_stop": "echo stop", - "unique_id": "unique", - }, - "not_unique_1": { - "command_open": "echo open", - "command_close": "echo close", - "command_stop": "echo stop", - "unique_id": "not-so-unique-anymore", - }, - "not_unique_2": { - "command_open": "echo open", - "command_close": "echo close", - "command_stop": "echo stop", - "unique_id": "not-so-unique-anymore", - }, - }, - ) - assert len(hass.states.async_all()) == 2 assert len(entity_registry.entities) == 2 diff --git a/tests/components/command_line/test_init.py b/tests/components/command_line/test_init.py new file mode 100644 index 00000000000..06d7b8c41dc --- /dev/null +++ b/tests/components/command_line/test_init.py @@ -0,0 +1,27 @@ +"""Test Command line component setup process.""" +from __future__ import annotations + +from datetime import timedelta + +from homeassistant.const import STATE_ON, STATE_OPEN +from homeassistant.core import HomeAssistant +import homeassistant.util.dt as dt_util + +from tests.common import async_fire_time_changed + + +async def test_setup_config(hass: HomeAssistant, load_yaml_integration: None) -> None: + """Test setup from yaml.""" + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) + await hass.async_block_till_done() + + state_binary_sensor = hass.states.get("binary_sensor.test") + state_sensor = hass.states.get("sensor.test") + state_cover = hass.states.get("cover.test") + state_switch = hass.states.get("switch.test") + + assert state_binary_sensor.state == STATE_ON + assert state_sensor.state == "5" + assert state_cover.state == STATE_OPEN + assert state_switch.state == STATE_ON diff --git a/tests/components/command_line/test_notify.py b/tests/components/command_line/test_notify.py index 918880a98fd..aeda981ce26 100644 --- a/tests/components/command_line/test_notify.py +++ b/tests/components/command_line/test_notify.py @@ -4,40 +4,71 @@ from __future__ import annotations import os import subprocess import tempfile -from typing import Any from unittest.mock import patch import pytest from homeassistant import setup -from homeassistant.components.notify import DOMAIN +from homeassistant.components.command_line import DOMAIN +from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN from homeassistant.core import HomeAssistant +import homeassistant.helpers.issue_registry as ir -async def setup_test_service(hass: HomeAssistant, config_dict: dict[str, Any]) -> None: - """Set up a test command line notify service.""" +async def test_setup_platform_yaml(hass: HomeAssistant) -> None: + """Test sensor setup.""" assert await setup.async_setup_component( hass, - DOMAIN, + NOTIFY_DOMAIN, { - DOMAIN: [ - {"platform": "command_line", "name": "Test", **config_dict}, + NOTIFY_DOMAIN: [ + {"platform": "command_line", "name": "Test1", "command": "exit 0"}, ] }, ) await hass.async_block_till_done() + assert hass.services.has_service(NOTIFY_DOMAIN, "test1") + + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue(DOMAIN, "deprecated_yaml_notify") + assert issue.translation_key == "deprecated_platform_yaml" -async def test_setup(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "get_config", + [ + { + "command_line": [ + { + "notify": { + "command": "exit 0", + "name": "Test2", + } + } + ] + } + ], +) +async def test_setup_integration_yaml( + hass: HomeAssistant, load_yaml_integration: None +) -> None: """Test sensor setup.""" - await setup_test_service(hass, {"command": "exit 0"}) - assert hass.services.has_service(DOMAIN, "test") + assert hass.services.has_service(NOTIFY_DOMAIN, "test2") async def test_bad_config(hass: HomeAssistant) -> None: """Test set up the platform with bad/missing configuration.""" - await setup_test_service(hass, {}) - assert not hass.services.has_service(DOMAIN, "test") + assert await setup.async_setup_component( + hass, + NOTIFY_DOMAIN, + { + NOTIFY_DOMAIN: [ + {"platform": "command_line"}, + ] + }, + ) + await hass.async_block_till_done() + assert not hass.services.has_service(NOTIFY_DOMAIN, "test") async def test_command_line_output(hass: HomeAssistant) -> None: @@ -45,58 +76,102 @@ async def test_command_line_output(hass: HomeAssistant) -> None: with tempfile.TemporaryDirectory() as tempdirname: filename = os.path.join(tempdirname, "message.txt") message = "one, two, testing, testing" - await setup_test_service( + await setup.async_setup_component( hass, + DOMAIN, { - "command": f"cat > {filename}", + "command_line": [ + { + "notify": { + "command": f"cat > {filename}", + "name": "Test3", + } + } + ] }, ) + await hass.async_block_till_done() - assert hass.services.has_service(DOMAIN, "test") + assert hass.services.has_service(NOTIFY_DOMAIN, "test3") assert await hass.services.async_call( - DOMAIN, "test", {"message": message}, blocking=True + NOTIFY_DOMAIN, "test3", {"message": message}, blocking=True ) - with open(filename) as handle: + with open(filename, encoding="UTF-8") as handle: # the echo command adds a line break assert message == handle.read() +@pytest.mark.parametrize( + "get_config", + [ + { + "command_line": [ + { + "notify": { + "command": "exit 1", + "name": "Test4", + } + } + ] + } + ], +) async def test_error_for_none_zero_exit_code( - caplog: pytest.LogCaptureFixture, hass: HomeAssistant + caplog: pytest.LogCaptureFixture, hass: HomeAssistant, load_yaml_integration: None ) -> None: """Test if an error is logged for non zero exit codes.""" - await setup_test_service( - hass, - { - "command": "exit 1", - }, - ) assert await hass.services.async_call( - DOMAIN, "test", {"message": "error"}, blocking=True + NOTIFY_DOMAIN, "test4", {"message": "error"}, blocking=True ) assert "Command failed" in caplog.text assert "return code 1" in caplog.text -async def test_timeout(caplog: pytest.LogCaptureFixture, hass: HomeAssistant) -> None: - """Test blocking is not forever.""" - await setup_test_service( - hass, +@pytest.mark.parametrize( + "get_config", + [ { - "command": "sleep 10000", - "command_timeout": 0.0000001, - }, - ) + "command_line": [ + { + "notify": { + "command": "sleep 10000", + "command_timeout": 0.0000001, + "name": "Test5", + } + } + ] + } + ], +) +async def test_timeout( + caplog: pytest.LogCaptureFixture, hass: HomeAssistant, load_yaml_integration: None +) -> None: + """Test blocking is not forever.""" assert await hass.services.async_call( - DOMAIN, "test", {"message": "error"}, blocking=True + NOTIFY_DOMAIN, "test5", {"message": "error"}, blocking=True ) assert "Timeout" in caplog.text +@pytest.mark.parametrize( + "get_config", + [ + { + "command_line": [ + { + "notify": { + "command": "exit 0", + "name": "Test6", + } + } + ] + } + ], +) async def test_subprocess_exceptions( - caplog: pytest.LogCaptureFixture, hass: HomeAssistant + caplog: pytest.LogCaptureFixture, hass: HomeAssistant, load_yaml_integration: None ) -> None: """Test that notify subprocess exceptions are handled correctly.""" @@ -110,15 +185,14 @@ async def test_subprocess_exceptions( subprocess.SubprocessError(), ] - await setup_test_service(hass, {"command": "exit 0"}) assert await hass.services.async_call( - DOMAIN, "test", {"message": "error"}, blocking=True + NOTIFY_DOMAIN, "test6", {"message": "error"}, blocking=True ) assert check_output.call_count == 2 assert "Timeout for command" in caplog.text assert await hass.services.async_call( - DOMAIN, "test", {"message": "error"}, blocking=True + NOTIFY_DOMAIN, "test6", {"message": "error"}, blocking=True ) assert check_output.call_count == 4 assert "Error trying to exec command" in caplog.text diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index 188f4aac062..c888f1894a0 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -8,38 +8,65 @@ from unittest.mock import patch import pytest from homeassistant import setup +from homeassistant.components.command_line import DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +import homeassistant.helpers.issue_registry as ir from homeassistant.util import dt from tests.common import async_fire_time_changed -async def setup_test_entities(hass: HomeAssistant, config_dict: dict[str, Any]) -> None: - """Set up a test command line sensor entity.""" - hass.states.async_set("sensor.input_sensor", "sensor_value") +async def test_setup_platform_yaml(hass: HomeAssistant) -> None: + """Test sensor setup.""" assert await setup.async_setup_component( hass, SENSOR_DOMAIN, { SENSOR_DOMAIN: [ - {"platform": "command_line", "name": "Test", **config_dict}, + { + "platform": "command_line", + "name": "Test", + "command": "echo 5", + "unit_of_measurement": "in", + }, ] }, ) await hass.async_block_till_done() + entity_state = hass.states.get("sensor.test") + assert entity_state + assert entity_state.state == "5" + assert entity_state.name == "Test" + assert entity_state.attributes["unit_of_measurement"] == "in" + + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue(DOMAIN, "deprecated_yaml_sensor") + assert issue.translation_key == "deprecated_platform_yaml" -async def test_setup(hass: HomeAssistant) -> None: - """Test sensor setup.""" - await setup_test_entities( - hass, +@pytest.mark.parametrize( + "get_config", + [ { - "command": "echo 5", - "unit_of_measurement": "in", - }, - ) + "command_line": [ + { + "sensor": { + "name": "Test", + "command": "echo 5", + "unit_of_measurement": "in", + } + } + ] + } + ], +) +async def test_setup_integration_yaml( + hass: HomeAssistant, load_yaml_integration: None +) -> None: + """Test sensor setup.""" + entity_state = hass.states.get("sensor.test") assert entity_state assert entity_state.state == "5" @@ -47,30 +74,51 @@ async def test_setup(hass: HomeAssistant) -> None: assert entity_state.attributes["unit_of_measurement"] == "in" -async def test_template(hass: HomeAssistant) -> None: - """Test command sensor with template.""" - await setup_test_entities( - hass, +@pytest.mark.parametrize( + "get_config", + [ { - "command": "echo 50", - "unit_of_measurement": "in", - "value_template": "{{ value | multiply(0.1) }}", - }, - ) + "command_line": [ + { + "sensor": { + "name": "Test", + "command": "echo 50", + "unit_of_measurement": "in", + "value_template": "{{ value | multiply(0.1) }}", + } + } + ] + } + ], +) +async def test_template(hass: HomeAssistant, load_yaml_integration: None) -> None: + """Test command sensor with template.""" + entity_state = hass.states.get("sensor.test") assert entity_state assert float(entity_state.state) == 5 -async def test_template_render(hass: HomeAssistant) -> None: - """Ensure command with templates get rendered properly.""" - - await setup_test_entities( - hass, +@pytest.mark.parametrize( + "get_config", + [ { - "command": "echo {{ states.sensor.input_sensor.state }}", - }, - ) + "command_line": [ + { + "sensor": { + "name": "Test", + "command": "echo {{ states.sensor.input_sensor.state }}", + } + } + ] + } + ], +) +async def test_template_render( + hass: HomeAssistant, load_yaml_integration: None +) -> None: + """Ensure command with templates get rendered properly.""" + hass.states.async_set("sensor.input_sensor", "sensor_value") # Give time for template to load async_fire_time_changed( @@ -86,18 +134,27 @@ async def test_template_render(hass: HomeAssistant) -> None: async def test_template_render_with_quote(hass: HomeAssistant) -> None: """Ensure command with templates and quotes get rendered properly.""" + hass.states.async_set("sensor.input_sensor", "sensor_value") + await setup.async_setup_component( + hass, + DOMAIN, + { + "command_line": [ + { + "sensor": { + "name": "Test", + "command": 'echo "{{ states.sensor.input_sensor.state }}" "3 4"', + } + } + ] + }, + ) + await hass.async_block_till_done() with patch( "homeassistant.components.command_line.utils.subprocess.check_output", return_value=b"Works\n", ) as check_output: - await setup_test_entities( - hass, - { - "command": 'echo "{{ states.sensor.input_sensor.state }}" "3 4"', - }, - ) - # Give time for template to load async_fire_time_changed( hass, @@ -105,7 +162,7 @@ async def test_template_render_with_quote(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert len(check_output.mock_calls) == 2 + assert len(check_output.mock_calls) == 1 check_output.assert_called_with( 'echo "sensor_value" "3 4"', shell=True, # nosec # shell by design @@ -114,59 +171,116 @@ async def test_template_render_with_quote(hass: HomeAssistant) -> None: ) +@pytest.mark.parametrize( + "get_config", + [ + { + "command_line": [ + { + "sensor": { + "name": "Test", + "command": "echo {{ this template doesn't parse", + } + } + ] + } + ], +) async def test_bad_template_render( - caplog: pytest.LogCaptureFixture, hass: HomeAssistant + caplog: pytest.LogCaptureFixture, hass: HomeAssistant, get_config: dict[str, Any] ) -> None: """Test rendering a broken template.""" - - await setup_test_entities( + await setup.async_setup_component( hass, - { - "command": "echo {{ this template doesn't parse", - }, + DOMAIN, + get_config, ) + await hass.async_block_till_done() assert "Error rendering command template" in caplog.text -async def test_bad_command(hass: HomeAssistant) -> None: - """Test bad command.""" - await setup_test_entities( - hass, +@pytest.mark.parametrize( + "get_config", + [ { - "command": "asdfasdf", - }, + "command_line": [ + { + "sensor": { + "name": "Test", + "command": "asdfasdf", + } + } + ] + } + ], +) +async def test_bad_command(hass: HomeAssistant, get_config: dict[str, Any]) -> None: + """Test bad command.""" + await setup.async_setup_component( + hass, + DOMAIN, + get_config, ) + await hass.async_block_till_done() + entity_state = hass.states.get("sensor.test") assert entity_state assert entity_state.state == "unknown" +@pytest.mark.parametrize( + "get_config", + [ + { + "command_line": [ + { + "sensor": { + "name": "Test", + "command": "exit 33", + } + } + ] + } + ], +) async def test_return_code( - caplog: pytest.LogCaptureFixture, hass: HomeAssistant + caplog: pytest.LogCaptureFixture, hass: HomeAssistant, get_config: dict[str, Any] ) -> None: """Test that an error return code is logged.""" - await setup_test_entities( + await setup.async_setup_component( hass, - { - "command": "exit 33", - }, + DOMAIN, + get_config, ) + await hass.async_block_till_done() + assert "return code 33" in caplog.text -async def test_update_with_json_attrs(hass: HomeAssistant) -> None: - """Test attributes get extracted from a JSON result.""" - await setup_test_entities( - hass, +@pytest.mark.parametrize( + "get_config", + [ { - "command": ( - 'echo { \\"key\\": \\"some_json_value\\", \\"another_key\\": ' - '\\"another_json_value\\", \\"key_three\\": \\"value_three\\" }' - ), - "json_attributes": ["key", "another_key", "key_three"], - }, - ) + "command_line": [ + { + "sensor": { + "name": "Test", + "command": ( + 'echo { \\"key\\": \\"some_json_value\\", \\"another_key\\": ' + '\\"another_json_value\\", \\"key_three\\": \\"value_three\\" }' + ), + "json_attributes": ["key", "another_key", "key_three"], + } + } + ] + } + ], +) +async def test_update_with_json_attrs( + hass: HomeAssistant, load_yaml_integration: None +) -> None: + """Test attributes get extracted from a JSON result.""" entity_state = hass.states.get("sensor.test") assert entity_state assert entity_state.state == "unknown" @@ -175,19 +289,30 @@ async def test_update_with_json_attrs(hass: HomeAssistant) -> None: assert entity_state.attributes["key_three"] == "value_three" -async def test_update_with_json_attrs_and_value_template(hass: HomeAssistant) -> None: - """Test json_attributes can be used together with value_template.""" - await setup_test_entities( - hass, +@pytest.mark.parametrize( + "get_config", + [ { - "command": ( - 'echo { \\"key\\": \\"some_json_value\\", \\"another_key\\": ' - '\\"another_json_value\\", \\"key_three\\": \\"value_three\\" }' - ), - "json_attributes": ["key", "another_key", "key_three"], - "value_template": '{{ value_json["key"] }}', - }, - ) + "command_line": [ + { + "sensor": { + "name": "Test", + "command": ( + 'echo { \\"key\\": \\"some_json_value\\", \\"another_key\\": ' + '\\"another_json_value\\", \\"key_three\\": \\"value_three\\" }' + ), + "json_attributes": ["key", "another_key", "key_three"], + "value_template": '{{ value_json["key"] }}', + } + } + ] + } + ], +) +async def test_update_with_json_attrs_and_value_template( + hass: HomeAssistant, load_yaml_integration: None +) -> None: + """Test json_attributes can be used together with value_template.""" entity_state = hass.states.get("sensor.test") assert entity_state assert entity_state.state == "some_json_value" @@ -196,75 +321,134 @@ async def test_update_with_json_attrs_and_value_template(hass: HomeAssistant) -> assert entity_state.attributes["key_three"] == "value_three" +@pytest.mark.parametrize( + "get_config", + [ + { + "command_line": [ + { + "sensor": { + "name": "Test", + "command": "echo", + "json_attributes": ["key"], + } + } + ] + } + ], +) async def test_update_with_json_attrs_no_data( - caplog: pytest.LogCaptureFixture, hass: HomeAssistant + caplog: pytest.LogCaptureFixture, hass: HomeAssistant, get_config: dict[str, Any] ) -> None: """Test attributes when no JSON result fetched.""" - - await setup_test_entities( + await setup.async_setup_component( hass, - { - "command": "echo", - "json_attributes": ["key"], - }, + DOMAIN, + get_config, ) + await hass.async_block_till_done() + entity_state = hass.states.get("sensor.test") assert entity_state assert "key" not in entity_state.attributes assert "Empty reply found when expecting JSON data" in caplog.text +@pytest.mark.parametrize( + "get_config", + [ + { + "command_line": [ + { + "sensor": { + "name": "Test", + "command": "echo [1, 2, 3]", + "json_attributes": ["key"], + } + } + ] + } + ], +) async def test_update_with_json_attrs_not_dict( - caplog: pytest.LogCaptureFixture, hass: HomeAssistant + caplog: pytest.LogCaptureFixture, hass: HomeAssistant, get_config: dict[str, Any] ) -> None: """Test attributes when the return value not a dict.""" - - await setup_test_entities( + await setup.async_setup_component( hass, - { - "command": "echo [1, 2, 3]", - "json_attributes": ["key"], - }, + DOMAIN, + get_config, ) + await hass.async_block_till_done() + entity_state = hass.states.get("sensor.test") assert entity_state assert "key" not in entity_state.attributes assert "JSON result was not a dictionary" in caplog.text +@pytest.mark.parametrize( + "get_config", + [ + { + "command_line": [ + { + "sensor": { + "name": "Test", + "command": "echo This is text rather than JSON data.", + "json_attributes": ["key"], + } + } + ] + } + ], +) async def test_update_with_json_attrs_bad_json( - caplog: pytest.LogCaptureFixture, hass: HomeAssistant + caplog: pytest.LogCaptureFixture, hass: HomeAssistant, get_config: dict[str, Any] ) -> None: """Test attributes when the return value is invalid JSON.""" - - await setup_test_entities( + await setup.async_setup_component( hass, - { - "command": "echo This is text rather than JSON data.", - "json_attributes": ["key"], - }, + DOMAIN, + get_config, ) + await hass.async_block_till_done() + entity_state = hass.states.get("sensor.test") assert entity_state assert "key" not in entity_state.attributes assert "Unable to parse output as JSON" in caplog.text +@pytest.mark.parametrize( + "get_config", + [ + { + "command_line": [ + { + "sensor": { + "name": "Test", + "command": ( + 'echo { \\"key\\": \\"some_json_value\\", \\"another_key\\": ' + '\\"another_json_value\\", \\"key_three\\": \\"value_three\\" }' + ), + "json_attributes": [ + "key", + "another_key", + "key_three", + "missing_key", + ], + } + } + ] + } + ], +) async def test_update_with_missing_json_attrs( - caplog: pytest.LogCaptureFixture, hass: HomeAssistant + caplog: pytest.LogCaptureFixture, hass: HomeAssistant, load_yaml_integration: None ) -> None: """Test attributes when an expected key is missing.""" - await setup_test_entities( - hass, - { - "command": ( - 'echo { \\"key\\": \\"some_json_value\\", \\"another_key\\": ' - '\\"another_json_value\\", \\"key_three\\": \\"value_three\\" }' - ), - "json_attributes": ["key", "another_key", "key_three", "missing_key"], - }, - ) entity_state = hass.states.get("sensor.test") assert entity_state assert entity_state.attributes["key"] == "some_json_value" @@ -273,21 +457,30 @@ async def test_update_with_missing_json_attrs( assert "missing_key" not in entity_state.attributes +@pytest.mark.parametrize( + "get_config", + [ + { + "command_line": [ + { + "sensor": { + "name": "Test", + "command": ( + 'echo { \\"key\\": \\"some_json_value\\", \\"another_key\\": ' + '\\"another_json_value\\", \\"key_three\\": \\"value_three\\" }' + ), + "json_attributes": ["key", "another_key"], + } + } + ] + } + ], +) async def test_update_with_unnecessary_json_attrs( - caplog: pytest.LogCaptureFixture, hass: HomeAssistant + caplog: pytest.LogCaptureFixture, hass: HomeAssistant, load_yaml_integration: None ) -> None: """Test attributes when an expected key is missing.""" - await setup_test_entities( - hass, - { - "command": ( - 'echo { \\"key\\": \\"some_json_value\\", \\"another_key\\": ' - '\\"another_json_value\\", \\"key_three\\": \\"value_three\\" }' - ), - "json_attributes": ["key", "another_key"], - }, - ) entity_state = hass.states.get("sensor.test") assert entity_state assert entity_state.attributes["key"] == "some_json_value" @@ -295,34 +488,40 @@ async def test_update_with_unnecessary_json_attrs( assert "key_three" not in entity_state.attributes -async def test_unique_id( - hass: HomeAssistant, entity_registry: er.EntityRegistry -) -> None: - """Test unique_id option and if it only creates one sensor per id.""" - assert await setup.async_setup_component( - hass, - SENSOR_DOMAIN, +@pytest.mark.parametrize( + "get_config", + [ { - SENSOR_DOMAIN: [ + "command_line": [ { - "platform": "command_line", - "unique_id": "unique", - "command": "echo 0", + "sensor": { + "name": "Test", + "unique_id": "unique", + "command": "echo 0", + } }, { - "platform": "command_line", - "unique_id": "not-so-unique-anymore", - "command": "echo 1", + "sensor": { + "name": "Test", + "unique_id": "not-so-unique-anymore", + "command": "echo 1", + } }, { - "platform": "command_line", - "unique_id": "not-so-unique-anymore", - "command": "echo 2", + "sensor": { + "name": "Test", + "unique_id": "not-so-unique-anymore", + "command": "echo 2", + }, }, ] - }, - ) - await hass.async_block_till_done() + } + ], +) +async def test_unique_id( + hass: HomeAssistant, entity_registry: er.EntityRegistry, load_yaml_integration: None +) -> None: + """Test unique_id option and if it only creates one sensor per id.""" assert len(hass.states.async_all()) == 2 diff --git a/tests/components/command_line/test_switch.py b/tests/components/command_line/test_switch.py index 69e78379afe..017c453aa8b 100644 --- a/tests/components/command_line/test_switch.py +++ b/tests/components/command_line/test_switch.py @@ -5,13 +5,13 @@ import json import os import subprocess import tempfile -from typing import Any from unittest.mock import patch import pytest from homeassistant import setup -from homeassistant.components.switch import DOMAIN, SCAN_INTERVAL +from homeassistant.components.command_line import DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SCAN_INTERVAL from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, @@ -21,45 +21,45 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +import homeassistant.helpers.issue_registry as ir import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed -async def setup_test_entity(hass: HomeAssistant, config_dict: dict[str, Any]) -> None: - """Set up a test command line switch entity.""" - assert await setup.async_setup_component( - hass, - DOMAIN, - { - DOMAIN: [ - {"platform": "command_line", "switches": config_dict}, - ] - }, - ) - await hass.async_block_till_done() - - -async def test_state_none(hass: HomeAssistant) -> None: +async def test_state_platform_yaml(hass: HomeAssistant) -> None: """Test with none state.""" with tempfile.TemporaryDirectory() as tempdirname: path = os.path.join(tempdirname, "switch_status") - await setup_test_entity( + assert await setup.async_setup_component( hass, + SWITCH_DOMAIN, { - "test": { - "command_on": f"echo 1 > {path}", - "command_off": f"echo 0 > {path}", - } + SWITCH_DOMAIN: [ + { + "platform": "command_line", + "switches": { + "test": { + "command_on": f"echo 1 > {path}", + "command_off": f"echo 0 > {path}", + "friendly_name": "Test", + "icon_template": ( + '{% if value=="1" %} mdi:on {% else %} mdi:off {% endif %}' + ), + } + }, + }, + ] }, ) + await hass.async_block_till_done() entity_state = hass.states.get("switch.test") assert entity_state assert entity_state.state == STATE_OFF await hass.services.async_call( - DOMAIN, + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: "switch.test"}, blocking=True, @@ -70,7 +70,7 @@ async def test_state_none(hass: HomeAssistant) -> None: assert entity_state.state == STATE_ON await hass.services.async_call( - DOMAIN, + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "switch.test"}, blocking=True, @@ -80,32 +80,69 @@ async def test_state_none(hass: HomeAssistant) -> None: assert entity_state assert entity_state.state == STATE_OFF + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue(DOMAIN, "deprecated_yaml_switch") + assert issue.translation_key == "deprecated_platform_yaml" + + +async def test_state_integration_yaml(hass: HomeAssistant) -> None: + """Test with none state.""" + with tempfile.TemporaryDirectory() as tempdirname: + path = os.path.join(tempdirname, "switch_status") + await setup.async_setup_component( + hass, + DOMAIN, + { + "command_line": [ + { + "switch": { + "command_on": f"echo 1 > {path}", + "command_off": f"echo 0 > {path}", + "name": "Test", + } + } + ] + }, + ) + await hass.async_block_till_done() + + entity_state = hass.states.get("switch.test") + assert entity_state + assert entity_state.state == STATE_OFF + async def test_state_value(hass: HomeAssistant) -> None: """Test with state value.""" with tempfile.TemporaryDirectory() as tempdirname: path = os.path.join(tempdirname, "switch_status") - await setup_test_entity( + await setup.async_setup_component( hass, + DOMAIN, { - "test": { - "command_state": f"cat {path}", - "command_on": f"echo 1 > {path}", - "command_off": f"echo 0 > {path}", - "value_template": '{{ value=="1" }}', - "icon_template": ( - '{% if value=="1" %} mdi:on {% else %} mdi:off {% endif %}' - ), - } + "command_line": [ + { + "switch": { + "command_state": f"cat {path}", + "command_on": f"echo 1 > {path}", + "command_off": f"echo 0 > {path}", + "value_template": '{{ value=="1" }}', + "icon": ( + '{% if value=="1" %} mdi:on {% else %} mdi:off {% endif %}' + ), + "name": "Test", + } + } + ] }, ) + await hass.async_block_till_done() entity_state = hass.states.get("switch.test") assert entity_state assert entity_state.state == STATE_OFF await hass.services.async_call( - DOMAIN, + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: "switch.test"}, blocking=True, @@ -117,7 +154,7 @@ async def test_state_value(hass: HomeAssistant) -> None: assert entity_state.attributes.get("icon") == "mdi:on" await hass.services.async_call( - DOMAIN, + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "switch.test"}, blocking=True, @@ -136,28 +173,35 @@ async def test_state_json_value(hass: HomeAssistant) -> None: oncmd = json.dumps({"status": "ok"}) offcmd = json.dumps({"status": "nope"}) - await setup_test_entity( + await setup.async_setup_component( hass, + DOMAIN, { - "test": { - "command_state": f"cat {path}", - "command_on": f"echo '{oncmd}' > {path}", - "command_off": f"echo '{offcmd}' > {path}", - "value_template": '{{ value_json.status=="ok" }}', - "icon_template": ( - '{% if value_json.status=="ok" %} mdi:on' - "{% else %} mdi:off {% endif %}" - ), - } + "command_line": [ + { + "switch": { + "command_state": f"cat {path}", + "command_on": f"echo '{oncmd}' > {path}", + "command_off": f"echo '{offcmd}' > {path}", + "value_template": '{{ value_json.status=="ok" }}', + "icon": ( + '{% if value_json.status=="ok" %} mdi:on' + "{% else %} mdi:off {% endif %}" + ), + "name": "Test", + } + } + ] }, ) + await hass.async_block_till_done() entity_state = hass.states.get("switch.test") assert entity_state assert entity_state.state == STATE_OFF await hass.services.async_call( - DOMAIN, + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: "switch.test"}, blocking=True, @@ -169,7 +213,7 @@ async def test_state_json_value(hass: HomeAssistant) -> None: assert entity_state.attributes.get("icon") == "mdi:on" await hass.services.async_call( - DOMAIN, + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "switch.test"}, blocking=True, @@ -185,23 +229,30 @@ async def test_state_code(hass: HomeAssistant) -> None: """Test with state code.""" with tempfile.TemporaryDirectory() as tempdirname: path = os.path.join(tempdirname, "switch_status") - await setup_test_entity( + await setup.async_setup_component( hass, + DOMAIN, { - "test": { - "command_state": f"cat {path}", - "command_on": f"echo 1 > {path}", - "command_off": f"echo 0 > {path}", - } + "command_line": [ + { + "switch": { + "command_state": f"cat {path}", + "command_on": f"echo 1 > {path}", + "command_off": f"echo 0 > {path}", + "name": "Test", + } + } + ] }, ) + await hass.async_block_till_done() entity_state = hass.states.get("switch.test") assert entity_state assert entity_state.state == STATE_OFF await hass.services.async_call( - DOMAIN, + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: "switch.test"}, blocking=True, @@ -212,7 +263,7 @@ async def test_state_code(hass: HomeAssistant) -> None: assert entity_state.state == STATE_ON await hass.services.async_call( - DOMAIN, + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "switch.test"}, blocking=True, @@ -228,15 +279,23 @@ async def test_assumed_state_should_be_true_if_command_state_is_none( ) -> None: """Test with state value.""" - await setup_test_entity( + await setup.async_setup_component( hass, + DOMAIN, { - "test": { - "command_on": "echo 'on command'", - "command_off": "echo 'off command'", - } + "command_line": [ + { + "switch": { + "command_on": "echo 'on command'", + "command_off": "echo 'off command'", + "name": "Test", + } + } + ] }, ) + await hass.async_block_till_done() + entity_state = hass.states.get("switch.test") assert entity_state assert entity_state.attributes["assumed_state"] @@ -247,16 +306,24 @@ async def test_assumed_state_should_absent_if_command_state_present( ) -> None: """Test with state value.""" - await setup_test_entity( + await setup.async_setup_component( hass, + DOMAIN, { - "test": { - "command_on": "echo 'on command'", - "command_off": "echo 'off command'", - "command_state": "cat {}", - } + "command_line": [ + { + "switch": { + "command_on": "echo 'on command'", + "command_off": "echo 'off command'", + "command_state": "cat {}", + "name": "Test", + } + } + ] }, ) + await hass.async_block_till_done() + entity_state = hass.states.get("switch.test") assert entity_state assert "assumed_state" not in entity_state.attributes @@ -264,18 +331,24 @@ async def test_assumed_state_should_absent_if_command_state_present( async def test_name_is_set_correctly(hass: HomeAssistant) -> None: """Test that name is set correctly.""" - await setup_test_entity( + await setup.async_setup_component( hass, + DOMAIN, { - "test": { - "command_on": "echo 'on command'", - "command_off": "echo 'off command'", - "friendly_name": "Test friendly name!", - } + "command_line": [ + { + "switch": { + "command_on": "echo 'on command'", + "command_off": "echo 'off command'", + "name": "Test friendly name!", + } + } + ] }, ) + await hass.async_block_till_done() - entity_state = hass.states.get("switch.test") + entity_state = hass.states.get("switch.test_friendly_name") assert entity_state assert entity_state.name == "Test friendly name!" @@ -284,16 +357,23 @@ async def test_switch_command_state_fail( caplog: pytest.LogCaptureFixture, hass: HomeAssistant ) -> None: """Test that switch failures are handled correctly.""" - await setup_test_entity( + await setup.async_setup_component( hass, + DOMAIN, { - "test": { - "command_on": "exit 0", - "command_off": "exit 0'", - "command_state": "echo 1", - } + "command_line": [ + { + "switch": { + "command_on": "exit 0", + "command_off": "exit 0'", + "command_state": "echo 1", + "name": "Test", + } + } + ] }, ) + await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() @@ -303,7 +383,7 @@ async def test_switch_command_state_fail( assert entity_state.state == "on" await hass.services.async_call( - DOMAIN, + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "switch.test"}, blocking=True, @@ -329,16 +409,24 @@ async def test_switch_command_state_code_exceptions( subprocess.SubprocessError(), ], ) as check_output: - await setup_test_entity( + await setup.async_setup_component( hass, + DOMAIN, { - "test": { - "command_on": "exit 0", - "command_off": "exit 0'", - "command_state": "echo 1", - } + "command_line": [ + { + "switch": { + "command_on": "exit 0", + "command_off": "exit 0'", + "command_state": "echo 1", + "name": "Test", + } + } + ] }, ) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() assert check_output.called @@ -362,17 +450,25 @@ async def test_switch_command_state_value_exceptions( subprocess.SubprocessError(), ], ) as check_output: - await setup_test_entity( + await setup.async_setup_component( hass, + DOMAIN, { - "test": { - "command_on": "exit 0", - "command_off": "exit 0'", - "command_state": "echo 1", - "value_template": '{{ value=="1" }}', - } + "command_line": [ + { + "switch": { + "command_on": "exit 0", + "command_off": "exit 0'", + "command_state": "echo 1", + "value_template": '{{ value=="1" }}', + "name": "Test", + } + } + ] }, ) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() assert check_output.call_count == 1 @@ -384,12 +480,24 @@ async def test_switch_command_state_value_exceptions( assert "Error trying to exec command" in caplog.text -async def test_no_switches( +async def test_no_switches_platform_yaml( caplog: pytest.LogCaptureFixture, hass: HomeAssistant ) -> None: """Test with no switches.""" - await setup_test_entity(hass, {}) + assert await setup.async_setup_component( + hass, + SWITCH_DOMAIN, + { + SWITCH_DOMAIN: [ + { + "platform": "command_line", + "switches": {}, + }, + ] + }, + ) + await hass.async_block_till_done() assert "No switches" in caplog.text @@ -397,26 +505,39 @@ async def test_unique_id( hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test unique_id option and if it only creates one switch per id.""" - await setup_test_entity( + await setup.async_setup_component( hass, + DOMAIN, { - "unique": { - "command_on": "echo on", - "command_off": "echo off", - "unique_id": "unique", - }, - "not_unique_1": { - "command_on": "echo on", - "command_off": "echo off", - "unique_id": "not-so-unique-anymore", - }, - "not_unique_2": { - "command_on": "echo on", - "command_off": "echo off", - "unique_id": "not-so-unique-anymore", - }, + "command_line": [ + { + "switch": { + "command_on": "echo on", + "command_off": "echo off", + "unique_id": "unique", + "name": "Test", + } + }, + { + "switch": { + "command_on": "echo on", + "command_off": "echo off", + "unique_id": "not-so-unique-anymore", + "name": "Test2", + } + }, + { + "switch": { + "command_on": "echo on", + "command_off": "echo off", + "unique_id": "not-so-unique-anymore", + "name": "Test3", + }, + }, + ] }, ) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 2 @@ -432,12 +553,24 @@ async def test_command_failure( ) -> None: """Test command failure.""" - await setup_test_entity( + await setup.async_setup_component( hass, - {"test": {"command_off": "exit 33"}}, + DOMAIN, + { + "command_line": [ + { + "switch": { + "command_off": "exit 33", + "name": "Test", + } + } + ] + }, ) + await hass.async_block_till_done() + await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "switch.test"}, blocking=True + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "switch.test"}, blocking=True ) assert "return code 33" in caplog.text @@ -446,29 +579,39 @@ async def test_templating(hass: HomeAssistant) -> None: """Test with templating.""" with tempfile.TemporaryDirectory() as tempdirname: path = os.path.join(tempdirname, "switch_status") - await setup_test_entity( + await setup.async_setup_component( hass, + DOMAIN, { - "test": { - "command_state": f"cat {path}", - "command_on": f"echo 1 > {path}", - "command_off": f"echo 0 > {path}", - "value_template": '{{ value=="1" }}', - "icon_template": ( - '{% if this.state=="on" %} mdi:on {% else %} mdi:off {% endif %}' - ), - }, - "test2": { - "command_state": f"cat {path}", - "command_on": f"echo 1 > {path}", - "command_off": f"echo 0 > {path}", - "value_template": '{{ value=="1" }}', - "icon_template": ( - '{% if states("switch.test2")=="on" %} mdi:on {% else %} mdi:off {% endif %}' - ), - }, + "command_line": [ + { + "switch": { + "command_state": f"cat {path}", + "command_on": f"echo 1 > {path}", + "command_off": f"echo 0 > {path}", + "value_template": '{{ value=="1" }}', + "icon": ( + '{% if this.state=="on" %} mdi:on {% else %} mdi:off {% endif %}' + ), + "name": "Test", + } + }, + { + "switch": { + "command_state": f"cat {path}", + "command_on": f"echo 1 > {path}", + "command_off": f"echo 0 > {path}", + "value_template": '{{ value=="1" }}', + "icon": ( + '{% if states("switch.test2")=="on" %} mdi:on {% else %} mdi:off {% endif %}' + ), + "name": "Test2", + }, + }, + ] }, ) + await hass.async_block_till_done() entity_state = hass.states.get("switch.test") entity_state2 = hass.states.get("switch.test2") @@ -476,13 +619,13 @@ async def test_templating(hass: HomeAssistant) -> None: assert entity_state2.state == STATE_OFF await hass.services.async_call( - DOMAIN, + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: "switch.test"}, blocking=True, ) await hass.services.async_call( - DOMAIN, + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: "switch.test2"}, blocking=True,