Skip to content

Commit 46e7486

Browse files
Move yaml configuration to integration key for command_line (home-assistant#92824)
* 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
1 parent 20d1a0f commit 46e7486

File tree

16 files changed

+1462
-490
lines changed

16 files changed

+1462
-490
lines changed

CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,8 @@ build.json @home-assistant/supervisor
213213
/tests/components/color_extractor/ @GenericStudent
214214
/homeassistant/components/comfoconnect/ @michaelarnauts
215215
/tests/components/comfoconnect/ @michaelarnauts
216+
/homeassistant/components/command_line/ @gjohansson-ST
217+
/tests/components/command_line/ @gjohansson-ST
216218
/homeassistant/components/compensation/ @Petro31
217219
/tests/components/compensation/ @Petro31
218220
/homeassistant/components/config/ @home-assistant/core
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,177 @@
11
"""The command_line component."""
2+
from __future__ import annotations
3+
4+
import asyncio
5+
from collections.abc import Coroutine
6+
import logging
7+
from typing import Any
8+
9+
import voluptuous as vol
10+
11+
from homeassistant.components.binary_sensor import (
12+
DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
13+
DOMAIN as BINARY_SENSOR_DOMAIN,
14+
)
15+
from homeassistant.components.cover import DOMAIN as COVER_DOMAIN
16+
from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN
17+
from homeassistant.components.sensor import (
18+
CONF_STATE_CLASS,
19+
DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA,
20+
DOMAIN as SENSOR_DOMAIN,
21+
STATE_CLASSES_SCHEMA as SENSOR_STATE_CLASSES_SCHEMA,
22+
)
23+
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
24+
from homeassistant.const import (
25+
CONF_COMMAND,
26+
CONF_COMMAND_CLOSE,
27+
CONF_COMMAND_OFF,
28+
CONF_COMMAND_ON,
29+
CONF_COMMAND_OPEN,
30+
CONF_COMMAND_STATE,
31+
CONF_COMMAND_STOP,
32+
CONF_DEVICE_CLASS,
33+
CONF_ICON,
34+
CONF_NAME,
35+
CONF_PAYLOAD_OFF,
36+
CONF_PAYLOAD_ON,
37+
CONF_UNIQUE_ID,
38+
CONF_UNIT_OF_MEASUREMENT,
39+
CONF_VALUE_TEMPLATE,
40+
Platform,
41+
)
42+
from homeassistant.core import HomeAssistant
43+
from homeassistant.helpers import discovery
44+
import homeassistant.helpers.config_validation as cv
45+
from homeassistant.helpers.reload import async_setup_reload_service
46+
from homeassistant.helpers.typing import ConfigType
47+
48+
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN
49+
50+
BINARY_SENSOR_DEFAULT_NAME = "Binary Command Sensor"
51+
DEFAULT_PAYLOAD_ON = "ON"
52+
DEFAULT_PAYLOAD_OFF = "OFF"
53+
CONF_JSON_ATTRIBUTES = "json_attributes"
54+
SENSOR_DEFAULT_NAME = "Command Sensor"
55+
CONF_NOTIFIERS = "notifiers"
56+
57+
PLATFORM_MAPPING = {
58+
BINARY_SENSOR_DOMAIN: Platform.BINARY_SENSOR,
59+
COVER_DOMAIN: Platform.COVER,
60+
NOTIFY_DOMAIN: Platform.NOTIFY,
61+
SENSOR_DOMAIN: Platform.SENSOR,
62+
SWITCH_DOMAIN: Platform.SWITCH,
63+
}
64+
65+
_LOGGER = logging.getLogger(__name__)
66+
67+
BINARY_SENSOR_SCHEMA = vol.Schema(
68+
{
69+
vol.Required(CONF_COMMAND): cv.string,
70+
vol.Optional(CONF_NAME, default=BINARY_SENSOR_DEFAULT_NAME): cv.string,
71+
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
72+
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
73+
vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
74+
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
75+
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
76+
vol.Optional(CONF_UNIQUE_ID): cv.string,
77+
}
78+
)
79+
COVER_SCHEMA = vol.Schema(
80+
{
81+
vol.Optional(CONF_COMMAND_CLOSE, default="true"): cv.string,
82+
vol.Optional(CONF_COMMAND_OPEN, default="true"): cv.string,
83+
vol.Optional(CONF_COMMAND_STATE): cv.string,
84+
vol.Optional(CONF_COMMAND_STOP, default="true"): cv.string,
85+
vol.Required(CONF_NAME): cv.string,
86+
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
87+
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
88+
vol.Optional(CONF_UNIQUE_ID): cv.string,
89+
}
90+
)
91+
NOTIFY_SCHEMA = vol.Schema(
92+
{
93+
vol.Required(CONF_COMMAND): cv.string,
94+
vol.Optional(CONF_NAME): cv.string,
95+
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
96+
}
97+
)
98+
SENSOR_SCHEMA = vol.Schema(
99+
{
100+
vol.Required(CONF_COMMAND): cv.string,
101+
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
102+
vol.Optional(CONF_JSON_ATTRIBUTES): cv.ensure_list_csv,
103+
vol.Optional(CONF_NAME, default=SENSOR_DEFAULT_NAME): cv.string,
104+
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
105+
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
106+
vol.Optional(CONF_UNIQUE_ID): cv.string,
107+
vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
108+
vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA,
109+
}
110+
)
111+
SWITCH_SCHEMA = vol.Schema(
112+
{
113+
vol.Optional(CONF_COMMAND_OFF, default="true"): cv.string,
114+
vol.Optional(CONF_COMMAND_ON, default="true"): cv.string,
115+
vol.Optional(CONF_COMMAND_STATE): cv.string,
116+
vol.Required(CONF_NAME): cv.string,
117+
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
118+
vol.Optional(CONF_ICON): cv.template,
119+
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
120+
vol.Optional(CONF_UNIQUE_ID): cv.string,
121+
}
122+
)
123+
COMBINED_SCHEMA = vol.Schema(
124+
{
125+
vol.Optional(BINARY_SENSOR_DOMAIN): BINARY_SENSOR_SCHEMA,
126+
vol.Optional(COVER_DOMAIN): COVER_SCHEMA,
127+
vol.Optional(NOTIFY_DOMAIN): NOTIFY_SCHEMA,
128+
vol.Optional(SENSOR_DOMAIN): SENSOR_SCHEMA,
129+
vol.Optional(SWITCH_DOMAIN): SWITCH_SCHEMA,
130+
}
131+
)
132+
CONFIG_SCHEMA = vol.Schema(
133+
{
134+
vol.Optional(DOMAIN): vol.All(
135+
cv.ensure_list,
136+
[COMBINED_SCHEMA],
137+
)
138+
},
139+
extra=vol.ALLOW_EXTRA,
140+
)
141+
142+
143+
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
144+
"""Set up Command Line from yaml config."""
145+
command_line_config: list[dict[str, dict[str, Any]]] = config.get(DOMAIN, {})
146+
if not command_line_config:
147+
return True
148+
149+
_LOGGER.debug("Full config loaded: %s", command_line_config)
150+
151+
load_coroutines: list[Coroutine[Any, Any, None]] = []
152+
platforms: list[Platform] = []
153+
for platform_config in command_line_config:
154+
for platform, _config in platform_config.items():
155+
platforms.append(PLATFORM_MAPPING[platform])
156+
_LOGGER.debug(
157+
"Loading config %s for platform %s",
158+
platform_config,
159+
PLATFORM_MAPPING[platform],
160+
)
161+
load_coroutines.append(
162+
discovery.async_load_platform(
163+
hass,
164+
PLATFORM_MAPPING[platform],
165+
DOMAIN,
166+
_config,
167+
config,
168+
)
169+
)
170+
171+
await async_setup_reload_service(hass, DOMAIN, platforms)
172+
173+
if load_coroutines:
174+
_LOGGER.debug("Loading platforms: %s", platforms)
175+
await asyncio.gather(*load_coroutines)
176+
177+
return True

homeassistant/components/command_line/binary_sensor.py

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from homeassistant.components.binary_sensor import (
99
DEVICE_CLASSES_SCHEMA,
10+
DOMAIN as BINARY_SENSOR_DOMAIN,
1011
PLATFORM_SCHEMA,
1112
BinarySensorDeviceClass,
1213
BinarySensorEntity,
@@ -23,11 +24,11 @@
2324
from homeassistant.core import HomeAssistant
2425
import homeassistant.helpers.config_validation as cv
2526
from homeassistant.helpers.entity_platform import AddEntitiesCallback
26-
from homeassistant.helpers.reload import async_setup_reload_service
27+
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
2728
from homeassistant.helpers.template import Template
2829
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
2930

30-
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, PLATFORMS
31+
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN
3132
from .sensor import CommandSensorData
3233

3334
DEFAULT_NAME = "Binary Command Sensor"
@@ -59,16 +60,30 @@ async def async_setup_platform(
5960
) -> None:
6061
"""Set up the Command line Binary Sensor."""
6162

62-
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
63-
64-
name: str = config.get(CONF_NAME, DEFAULT_NAME)
65-
command: str = config[CONF_COMMAND]
66-
payload_off: str = config[CONF_PAYLOAD_OFF]
67-
payload_on: str = config[CONF_PAYLOAD_ON]
68-
device_class: BinarySensorDeviceClass | None = config.get(CONF_DEVICE_CLASS)
69-
value_template: Template | None = config.get(CONF_VALUE_TEMPLATE)
70-
command_timeout: int = config[CONF_COMMAND_TIMEOUT]
71-
unique_id: str | None = config.get(CONF_UNIQUE_ID)
63+
if binary_sensor_config := config:
64+
async_create_issue(
65+
hass,
66+
DOMAIN,
67+
"deprecated_yaml_binary_sensor",
68+
breaks_in_ha_version="2023.8.0",
69+
is_fixable=False,
70+
severity=IssueSeverity.WARNING,
71+
translation_key="deprecated_platform_yaml",
72+
translation_placeholders={"platform": BINARY_SENSOR_DOMAIN},
73+
)
74+
if discovery_info:
75+
binary_sensor_config = discovery_info
76+
77+
name: str = binary_sensor_config.get(CONF_NAME, DEFAULT_NAME)
78+
command: str = binary_sensor_config[CONF_COMMAND]
79+
payload_off: str = binary_sensor_config[CONF_PAYLOAD_OFF]
80+
payload_on: str = binary_sensor_config[CONF_PAYLOAD_ON]
81+
device_class: BinarySensorDeviceClass | None = binary_sensor_config.get(
82+
CONF_DEVICE_CLASS
83+
)
84+
value_template: Template | None = binary_sensor_config.get(CONF_VALUE_TEMPLATE)
85+
command_timeout: int = binary_sensor_config[CONF_COMMAND_TIMEOUT]
86+
unique_id: str | None = binary_sensor_config.get(CONF_UNIQUE_ID)
7287
if value_template is not None:
7388
value_template.hass = hass
7489
data = CommandSensorData(hass, command, command_timeout)

homeassistant/components/command_line/cover.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,31 @@
66

77
import voluptuous as vol
88

9-
from homeassistant.components.cover import PLATFORM_SCHEMA, CoverEntity
9+
from homeassistant.components.cover import (
10+
DOMAIN as COVER_DOMAIN,
11+
PLATFORM_SCHEMA,
12+
CoverEntity,
13+
)
1014
from homeassistant.const import (
1115
CONF_COMMAND_CLOSE,
1216
CONF_COMMAND_OPEN,
1317
CONF_COMMAND_STATE,
1418
CONF_COMMAND_STOP,
1519
CONF_COVERS,
1620
CONF_FRIENDLY_NAME,
21+
CONF_NAME,
1722
CONF_UNIQUE_ID,
1823
CONF_VALUE_TEMPLATE,
1924
)
2025
from homeassistant.core import HomeAssistant
2126
import homeassistant.helpers.config_validation as cv
2227
from homeassistant.helpers.entity_platform import AddEntitiesCallback
23-
from homeassistant.helpers.reload import async_setup_reload_service
28+
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
2429
from homeassistant.helpers.template import Template
2530
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
31+
from homeassistant.util import slugify
2632

27-
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, PLATFORMS
33+
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN
2834
from .utils import call_shell_with_timeout, check_output_or_log
2935

3036
_LOGGER = logging.getLogger(__name__)
@@ -55,19 +61,35 @@ async def async_setup_platform(
5561
) -> None:
5662
"""Set up cover controlled by shell commands."""
5763

58-
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
59-
60-
devices: dict[str, Any] = config.get(CONF_COVERS, {})
6164
covers = []
65+
if discovery_info:
66+
entities: dict[str, Any] = {slugify(discovery_info[CONF_NAME]): discovery_info}
67+
else:
68+
async_create_issue(
69+
hass,
70+
DOMAIN,
71+
"deprecated_yaml_cover",
72+
breaks_in_ha_version="2023.8.0",
73+
is_fixable=False,
74+
severity=IssueSeverity.WARNING,
75+
translation_key="deprecated_platform_yaml",
76+
translation_placeholders={"platform": COVER_DOMAIN},
77+
)
78+
entities = config.get(CONF_COVERS, {})
6279

63-
for device_name, device_config in devices.items():
80+
for device_name, device_config in entities.items():
6481
value_template: Template | None = device_config.get(CONF_VALUE_TEMPLATE)
6582
if value_template is not None:
6683
value_template.hass = hass
6784

85+
if name := device_config.get(
86+
CONF_FRIENDLY_NAME
87+
): # Backward compatibility. Can be removed after deprecation
88+
device_config[CONF_NAME] = name
89+
6890
covers.append(
6991
CommandCover(
70-
device_config.get(CONF_FRIENDLY_NAME, device_name),
92+
device_config.get(CONF_NAME, device_name),
7193
device_config[CONF_COMMAND_OPEN],
7294
device_config[CONF_COMMAND_CLOSE],
7395
device_config[CONF_COMMAND_STOP],
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"domain": "command_line",
33
"name": "Command Line",
4-
"codeowners": [],
4+
"codeowners": ["@gjohansson-ST"],
55
"documentation": "https://www.home-assistant.io/integrations/command_line",
66
"iot_class": "local_polling"
77
}

homeassistant/components/command_line/notify.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,19 @@
77

88
import voluptuous as vol
99

10-
from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService
10+
from homeassistant.components.notify import (
11+
DOMAIN as NOTIFY_DOMAIN,
12+
PLATFORM_SCHEMA,
13+
BaseNotificationService,
14+
)
1115
from homeassistant.const import CONF_COMMAND, CONF_NAME
1216
from homeassistant.core import HomeAssistant
1317
import homeassistant.helpers.config_validation as cv
18+
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
1419
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
1520
from homeassistant.util.process import kill_subprocess
1621

17-
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT
22+
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN
1823

1924
_LOGGER = logging.getLogger(__name__)
2025

@@ -33,8 +38,21 @@ def get_service(
3338
discovery_info: DiscoveryInfoType | None = None,
3439
) -> CommandLineNotificationService:
3540
"""Get the Command Line notification service."""
36-
command: str = config[CONF_COMMAND]
37-
timeout: int = config[CONF_COMMAND_TIMEOUT]
41+
if notify_config := config:
42+
create_issue(
43+
hass,
44+
DOMAIN,
45+
"deprecated_yaml_notify",
46+
breaks_in_ha_version="2023.8.0",
47+
is_fixable=False,
48+
severity=IssueSeverity.WARNING,
49+
translation_key="deprecated_platform_yaml",
50+
translation_placeholders={"platform": NOTIFY_DOMAIN},
51+
)
52+
if discovery_info:
53+
notify_config = discovery_info
54+
command: str = notify_config[CONF_COMMAND]
55+
timeout: int = notify_config[CONF_COMMAND_TIMEOUT]
3856

3957
return CommandLineNotificationService(command, timeout)
4058

0 commit comments

Comments
 (0)