From d26275011ae4e8ba0a8dcdc2a7ef81b5911d3900 Mon Sep 17 00:00:00 2001 From: Gabriel Rauter Date: Mon, 3 Jan 2022 11:44:47 +0100 Subject: [PATCH] Add unique_id configuration variable to command_line integration (#58596) --- .../components/command_line/binary_sensor.py | 23 +++++++++- .../components/command_line/cover.py | 5 +++ .../components/command_line/sensor.py | 20 ++++++++- .../components/command_line/switch.py | 5 +++ .../command_line/test_binary_sensor.py | 45 +++++++++++++++++++ tests/components/command_line/test_cover.py | 39 ++++++++++++++++ tests/components/command_line/test_sensor.py | 40 +++++++++++++++++ tests/components/command_line/test_switch.py | 36 +++++++++++++++ 8 files changed, 209 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/command_line/binary_sensor.py b/homeassistant/components/command_line/binary_sensor.py index 0cfb35cf008..e7cfe5288d8 100644 --- a/homeassistant/components/command_line/binary_sensor.py +++ b/homeassistant/components/command_line/binary_sensor.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, + CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) from homeassistant.core import HomeAssistant @@ -43,6 +44,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_DEVICE_CLASS): 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, } ) @@ -64,6 +66,7 @@ def setup_platform( device_class = config.get(CONF_DEVICE_CLASS) value_template = config.get(CONF_VALUE_TEMPLATE) command_timeout = config.get(CONF_COMMAND_TIMEOUT) + unique_id = config.get(CONF_UNIQUE_ID) if value_template is not None: value_template.hass = hass data = CommandSensorData(hass, command, command_timeout) @@ -71,7 +74,14 @@ def setup_platform( add_entities( [ CommandBinarySensor( - hass, data, name, device_class, payload_on, payload_off, value_template + hass, + data, + name, + device_class, + payload_on, + payload_off, + value_template, + unique_id, ) ], True, @@ -82,7 +92,15 @@ class CommandBinarySensor(BinarySensorEntity): """Representation of a command line binary sensor.""" def __init__( - self, hass, data, name, device_class, payload_on, payload_off, value_template + self, + hass, + data, + name, + device_class, + payload_on, + payload_off, + value_template, + unique_id, ): """Initialize the Command line binary sensor.""" self._hass = hass @@ -93,6 +111,7 @@ class CommandBinarySensor(BinarySensorEntity): self._payload_on = payload_on self._payload_off = payload_off self._value_template = value_template + self._attr_unique_id = unique_id @property def name(self): diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index 0acebd90d80..ff00138eba4 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -13,6 +13,7 @@ from homeassistant.const import ( CONF_COMMAND_STOP, CONF_COVERS, CONF_FRIENDLY_NAME, + CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) from homeassistant.core import HomeAssistant @@ -35,6 +36,7 @@ COVER_SCHEMA = vol.Schema( vol.Optional(CONF_FRIENDLY_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, } ) @@ -71,6 +73,7 @@ def setup_platform( device_config.get(CONF_COMMAND_STATE), value_template, device_config[CONF_COMMAND_TIMEOUT], + device_config.get(CONF_UNIQUE_ID), ) ) @@ -94,6 +97,7 @@ class CommandCover(CoverEntity): command_state, value_template, timeout, + unique_id, ): """Initialize the cover.""" self._hass = hass @@ -105,6 +109,7 @@ class CommandCover(CoverEntity): self._command_state = command_state self._value_template = value_template self._timeout = timeout + self._attr_unique_id = unique_id def _move_cover(self, command): """Execute the actual commands.""" diff --git a/homeassistant/components/command_line/sensor.py b/homeassistant/components/command_line/sensor.py index 824b597ac41..387dacfc8de 100644 --- a/homeassistant/components/command_line/sensor.py +++ b/homeassistant/components/command_line/sensor.py @@ -12,6 +12,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_COMMAND, CONF_NAME, + CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, @@ -43,6 +44,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_NAME, default=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, } ) @@ -62,13 +64,19 @@ def setup_platform( unit = config.get(CONF_UNIT_OF_MEASUREMENT) value_template = config.get(CONF_VALUE_TEMPLATE) command_timeout = config.get(CONF_COMMAND_TIMEOUT) + unique_id = config.get(CONF_UNIQUE_ID) if value_template is not None: value_template.hass = hass json_attributes = config.get(CONF_JSON_ATTRIBUTES) data = CommandSensorData(hass, command, command_timeout) add_entities( - [CommandSensor(hass, data, name, unit, value_template, json_attributes)], True + [ + CommandSensor( + hass, data, name, unit, value_template, json_attributes, unique_id + ) + ], + True, ) @@ -76,7 +84,14 @@ class CommandSensor(SensorEntity): """Representation of a sensor that is using shell commands.""" def __init__( - self, hass, data, name, unit_of_measurement, value_template, json_attributes + self, + hass, + data, + name, + unit_of_measurement, + value_template, + json_attributes, + unique_id, ): """Initialize the sensor.""" self._hass = hass @@ -87,6 +102,7 @@ class CommandSensor(SensorEntity): self._state = None self._unit_of_measurement = unit_of_measurement self._value_template = value_template + self._attr_unique_id = unique_id @property def name(self): diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index 3fc9068c937..e65db8afcc8 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -17,6 +17,7 @@ from homeassistant.const import ( CONF_FRIENDLY_NAME, CONF_ICON_TEMPLATE, CONF_SWITCHES, + CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) from homeassistant.core import HomeAssistant @@ -39,6 +40,7 @@ SWITCH_SCHEMA = vol.Schema( vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_ICON_TEMPLATE): cv.template, vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + vol.Optional(CONF_UNIQUE_ID): cv.string, } ) @@ -81,6 +83,7 @@ def setup_platform( icon_template, value_template, device_config[CONF_COMMAND_TIMEOUT], + device_config.get(CONF_UNIQUE_ID), ) ) @@ -105,6 +108,7 @@ class CommandSwitch(SwitchEntity): icon_template, value_template, timeout, + unique_id, ): """Initialize the switch.""" self._hass = hass @@ -117,6 +121,7 @@ class CommandSwitch(SwitchEntity): self._icon_template = icon_template self._value_template = value_template self._timeout = timeout + self._attr_unique_id = unique_id def _switch(self, command): """Execute the actual commands.""" diff --git a/tests/components/command_line/test_binary_sensor.py b/tests/components/command_line/test_binary_sensor.py index 2749fff8127..532e14573f4 100644 --- a/tests/components/command_line/test_binary_sensor.py +++ b/tests/components/command_line/test_binary_sensor.py @@ -7,6 +7,7 @@ from homeassistant import setup from homeassistant.components.binary_sensor import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry async def setup_test_entity(hass: HomeAssistant, config_dict: dict[str, Any]) -> None: @@ -65,3 +66,47 @@ async def test_sensor_off(hass: HomeAssistant) -> None: ) entity_state = hass.states.get("binary_sensor.test") assert entity_state.state == STATE_OFF + + +async def test_unique_id(hass): + """Test unique_id option and if it only creates one binary sensor per id.""" + assert await setup.async_setup_component( + hass, + DOMAIN, + { + DOMAIN: [ + { + "platform": "command_line", + "unique_id": "unique", + "command": "echo 0", + }, + { + "platform": "command_line", + "unique_id": "not-so-unique-anymore", + "command": "echo 1", + }, + { + "platform": "command_line", + "unique_id": "not-so-unique-anymore", + "command": "echo 2", + }, + ] + }, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 2 + + ent_reg = entity_registry.async_get(hass) + + assert len(ent_reg.entities) == 2 + assert ( + ent_reg.async_get_entity_id("binary_sensor", "command_line", "unique") + is not None + ) + assert ( + ent_reg.async_get_entity_id( + "binary_sensor", "command_line", "not-so-unique-anymore" + ) + is not None + ) diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py index 0a37449c184..9d4f5b60c8b 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -16,6 +16,7 @@ from homeassistant.const import ( SERVICE_STOP_COVER, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed, get_fixture_path @@ -160,3 +161,41 @@ async def test_move_cover_failure(caplog: Any, hass: HomeAssistant) -> None: DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: "cover.test"}, blocking=True ) assert "Command failed" in caplog.text + + +async def test_unique_id(hass): + """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 + + ent_reg = entity_registry.async_get(hass) + + assert len(ent_reg.entities) == 2 + assert ent_reg.async_get_entity_id("cover", "command_line", "unique") is not None + assert ( + ent_reg.async_get_entity_id("cover", "command_line", "not-so-unique-anymore") + is not None + ) diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index be897fa2408..c9a4860b987 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -7,6 +7,7 @@ from unittest.mock import patch from homeassistant import setup from homeassistant.components.sensor import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry async def setup_test_entities(hass: HomeAssistant, config_dict: dict[str, Any]) -> None: @@ -223,3 +224,42 @@ async def test_update_with_unnecessary_json_attrs(caplog, hass: HomeAssistant) - assert entity_state.attributes["key"] == "some_json_value" assert entity_state.attributes["another_key"] == "another_json_value" assert "key_three" not in entity_state.attributes + + +async def test_unique_id(hass): + """Test unique_id option and if it only creates one sensor per id.""" + assert await setup.async_setup_component( + hass, + DOMAIN, + { + DOMAIN: [ + { + "platform": "command_line", + "unique_id": "unique", + "command": "echo 0", + }, + { + "platform": "command_line", + "unique_id": "not-so-unique-anymore", + "command": "echo 1", + }, + { + "platform": "command_line", + "unique_id": "not-so-unique-anymore", + "command": "echo 2", + }, + ] + }, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 2 + + ent_reg = entity_registry.async_get(hass) + + assert len(ent_reg.entities) == 2 + assert ent_reg.async_get_entity_id("sensor", "command_line", "unique") is not None + assert ( + ent_reg.async_get_entity_id("sensor", "command_line", "not-so-unique-anymore") + is not None + ) diff --git a/tests/components/command_line/test_switch.py b/tests/components/command_line/test_switch.py index 910d990d07d..f918c7500ad 100644 --- a/tests/components/command_line/test_switch.py +++ b/tests/components/command_line/test_switch.py @@ -18,6 +18,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed @@ -376,3 +377,38 @@ async def test_no_switches(caplog: Any, hass: HomeAssistant) -> None: await setup_test_entity(hass, {}) assert "No switches" in caplog.text + + +async def test_unique_id(hass): + """Test unique_id option and if it only creates one switch per id.""" + await setup_test_entity( + hass, + { + "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", + }, + }, + ) + + assert len(hass.states.async_all()) == 2 + + ent_reg = entity_registry.async_get(hass) + + assert len(ent_reg.entities) == 2 + assert ent_reg.async_get_entity_id("switch", "command_line", "unique") is not None + assert ( + ent_reg.async_get_entity_id("switch", "command_line", "not-so-unique-anymore") + is not None + )