Skip to content

Commit

Permalink
Move yaml configuration to integration key for command_line (#92824)
Browse files Browse the repository at this point in the history
* Inital init commit

* bs and cover

* notify

* sensor

* switch

* Issues

* Finalize __init__

* First pass tests

* Fix Binary sensors

* Test cover

* Test notify

* Test sensor

* Tests switch

* Fix coverage

* Add codeowner

* Fix caplog

* test issue

* Flaky test notify

* Fix async

* Adjust yaml structure

* Change yaml format again

* Issue strings

* Fix tests

* string review comment
  • Loading branch information
gjohansson-ST committed May 29, 2023
1 parent 20d1a0f commit 46e7486
Show file tree
Hide file tree
Showing 16 changed files with 1,462 additions and 490 deletions.
2 changes: 2 additions & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
176 changes: 176 additions & 0 deletions homeassistant/components/command_line/__init__.py
Original file line number Diff line number Diff line change
@@ -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
39 changes: 27 additions & 12 deletions homeassistant/components/command_line/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from homeassistant.components.binary_sensor import (
DEVICE_CLASSES_SCHEMA,
DOMAIN as BINARY_SENSOR_DOMAIN,
PLATFORM_SCHEMA,
BinarySensorDeviceClass,
BinarySensorEntity,
Expand All @@ -23,11 +24,11 @@
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"
Expand Down Expand Up @@ -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)

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)
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 = 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)
Expand Down
38 changes: 30 additions & 8 deletions homeassistant/components/command_line/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,31 @@

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,
CONF_COMMAND_STATE,
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__)
Expand Down Expand Up @@ -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],
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/command_line/manifest.json
Original file line number Diff line number Diff line change
@@ -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"
}
26 changes: 22 additions & 4 deletions homeassistant/components/command_line/notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@

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__)

Expand All @@ -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)

Expand Down

0 comments on commit 46e7486

Please sign in to comment.