Add scan interval to Command Line (#93752)

* Add scan interval

* Handle previous not complete

* Fix faulty text

* Add tests

* lingering

* Cool down

* Fix tests
This commit is contained in:
G Johansson 2023-06-03 05:35:11 +02:00 committed by GitHub
parent aa636a2805
commit 038b0e6d23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 463 additions and 41 deletions

View File

@ -11,16 +11,24 @@ import voluptuous as vol
from homeassistant.components.binary_sensor import (
DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
DOMAIN as BINARY_SENSOR_DOMAIN,
SCAN_INTERVAL as BINARY_SENSOR_DEFAULT_SCAN_INTERVAL,
)
from homeassistant.components.cover import (
DOMAIN as COVER_DOMAIN,
SCAN_INTERVAL as COVER_DEFAULT_SCAN_INTERVAL,
)
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,
SCAN_INTERVAL as SENSOR_DEFAULT_SCAN_INTERVAL,
STATE_CLASSES_SCHEMA as SENSOR_STATE_CLASSES_SCHEMA,
)
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
SCAN_INTERVAL as SWITCH_DEFAULT_SCAN_INTERVAL,
)
from homeassistant.const import (
CONF_COMMAND,
CONF_COMMAND_CLOSE,
@ -34,6 +42,7 @@ from homeassistant.const import (
CONF_NAME,
CONF_PAYLOAD_OFF,
CONF_PAYLOAD_ON,
CONF_SCAN_INTERVAL,
CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE_TEMPLATE,
@ -74,6 +83,9 @@ BINARY_SENSOR_SCHEMA = vol.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,
vol.Optional(
CONF_SCAN_INTERVAL, default=BINARY_SENSOR_DEFAULT_SCAN_INTERVAL
): vol.All(cv.time_period, cv.positive_timedelta),
}
)
COVER_SCHEMA = vol.Schema(
@ -86,6 +98,9 @@ COVER_SCHEMA = vol.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,
vol.Optional(CONF_SCAN_INTERVAL, default=COVER_DEFAULT_SCAN_INTERVAL): vol.All(
cv.time_period, cv.positive_timedelta
),
}
)
NOTIFY_SCHEMA = vol.Schema(
@ -106,6 +121,9 @@ SENSOR_SCHEMA = vol.Schema(
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,
vol.Optional(CONF_SCAN_INTERVAL, default=SENSOR_DEFAULT_SCAN_INTERVAL): vol.All(
cv.time_period, cv.positive_timedelta
),
}
)
SWITCH_SCHEMA = vol.Schema(
@ -118,6 +136,9 @@ SWITCH_SCHEMA = vol.Schema(
vol.Optional(CONF_ICON): cv.template,
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=SWITCH_DEFAULT_SCAN_INTERVAL): vol.All(
cv.time_period, cv.positive_timedelta
),
}
)
COMBINED_SCHEMA = vol.Schema(

View File

@ -1,6 +1,7 @@
"""Support for custom shell commands to retrieve values."""
from __future__ import annotations
import asyncio
from datetime import timedelta
import voluptuous as vol
@ -18,17 +19,19 @@ from homeassistant.const import (
CONF_NAME,
CONF_PAYLOAD_OFF,
CONF_PAYLOAD_ON,
CONF_SCAN_INTERVAL,
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.event import async_track_time_interval
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
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
from .sensor import CommandSensorData
DEFAULT_NAME = "Binary Command Sensor"
@ -84,6 +87,9 @@ async def async_setup_platform(
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)
scan_interval: timedelta = binary_sensor_config.get(
CONF_SCAN_INTERVAL, SCAN_INTERVAL
)
if value_template is not None:
value_template.hass = hass
data = CommandSensorData(hass, command, command_timeout)
@ -98,6 +104,7 @@ async def async_setup_platform(
payload_off,
value_template,
unique_id,
scan_interval,
)
],
True,
@ -107,6 +114,8 @@ async def async_setup_platform(
class CommandBinarySensor(BinarySensorEntity):
"""Representation of a command line binary sensor."""
_attr_should_poll = False
def __init__(
self,
data: CommandSensorData,
@ -116,6 +125,7 @@ class CommandBinarySensor(BinarySensorEntity):
payload_off: str,
value_template: Template | None,
unique_id: str | None,
scan_interval: timedelta,
) -> None:
"""Initialize the Command line binary sensor."""
self.data = data
@ -126,8 +136,39 @@ class CommandBinarySensor(BinarySensorEntity):
self._payload_off = payload_off
self._value_template = value_template
self._attr_unique_id = unique_id
self._scan_interval = scan_interval
self._process_updates: asyncio.Lock | None = None
async def async_update(self) -> None:
async def async_added_to_hass(self) -> None:
"""Call when entity about to be added to hass."""
await super().async_added_to_hass()
await self._update_entity_state(None)
self.async_on_remove(
async_track_time_interval(
self.hass,
self._update_entity_state,
self._scan_interval,
name=f"Command Line Binary Sensor - {self.name}",
cancel_on_shutdown=True,
),
)
async def _update_entity_state(self, now) -> None:
"""Update the state of the entity."""
if self._process_updates is None:
self._process_updates = asyncio.Lock()
if self._process_updates.locked():
LOGGER.warning(
"Updating Command Line Binary Sensor %s took longer than the scheduled update interval %s",
self.name,
self._scan_interval,
)
return
async with self._process_updates:
await self._async_update()
async def _async_update(self) -> None:
"""Get the latest data and updates the state."""
await self.hass.async_add_executor_job(self.data.update)
value = self.data.value
@ -141,3 +182,5 @@ class CommandBinarySensor(BinarySensorEntity):
self._attr_is_on = True
elif value == self._payload_off:
self._attr_is_on = False
self.async_write_ha_state()

View File

@ -1,7 +1,11 @@
"""Allows to configure custom shell commands to turn a value for a sensor."""
import logging
from homeassistant.const import Platform
LOGGER = logging.getLogger(__package__)
CONF_COMMAND_TIMEOUT = "command_timeout"
DEFAULT_TIMEOUT = 15
DOMAIN = "command_line"

View File

@ -1,7 +1,8 @@
"""Support for command line covers."""
from __future__ import annotations
import logging
import asyncio
from datetime import timedelta
from typing import TYPE_CHECKING, Any
import voluptuous as vol
@ -19,21 +20,23 @@ from homeassistant.const import (
CONF_COVERS,
CONF_FRIENDLY_NAME,
CONF_NAME,
CONF_SCAN_INTERVAL,
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.event import async_track_time_interval
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
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
from .utils import call_shell_with_timeout, check_output_or_log
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=15)
COVER_SCHEMA = vol.Schema(
{
@ -97,11 +100,12 @@ async def async_setup_platform(
value_template,
device_config[CONF_COMMAND_TIMEOUT],
device_config.get(CONF_UNIQUE_ID),
device_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL),
)
)
if not covers:
_LOGGER.error("No covers added")
LOGGER.error("No covers added")
return
async_add_entities(covers)
@ -110,6 +114,8 @@ async def async_setup_platform(
class CommandCover(CoverEntity):
"""Representation a command line cover."""
_attr_should_poll = False
def __init__(
self,
name: str,
@ -120,6 +126,7 @@ class CommandCover(CoverEntity):
value_template: Template | None,
timeout: int,
unique_id: str | None,
scan_interval: timedelta,
) -> None:
"""Initialize the cover."""
self._attr_name = name
@ -131,17 +138,32 @@ class CommandCover(CoverEntity):
self._value_template = value_template
self._timeout = timeout
self._attr_unique_id = unique_id
self._attr_should_poll = bool(command_state)
self._scan_interval = scan_interval
self._process_updates: asyncio.Lock | None = None
async def async_added_to_hass(self) -> None:
"""Call when entity about to be added to hass."""
await super().async_added_to_hass()
if self._command_state:
self.async_on_remove(
async_track_time_interval(
self.hass,
self._update_entity_state,
self._scan_interval,
name=f"Command Line Cover - {self.name}",
cancel_on_shutdown=True,
),
)
def _move_cover(self, command: str) -> bool:
"""Execute the actual commands."""
_LOGGER.info("Running command: %s", command)
LOGGER.info("Running command: %s", command)
returncode = call_shell_with_timeout(command, self._timeout)
success = returncode == 0
if not success:
_LOGGER.error(
LOGGER.error(
"Command failed (with return code %s): %s", returncode, command
)
@ -165,12 +187,27 @@ class CommandCover(CoverEntity):
def _query_state(self) -> str | None:
"""Query for the state."""
if self._command_state:
_LOGGER.info("Running state value command: %s", self._command_state)
LOGGER.info("Running state value command: %s", self._command_state)
return check_output_or_log(self._command_state, self._timeout)
if TYPE_CHECKING:
return None
async def async_update(self) -> None:
async def _update_entity_state(self, now) -> None:
"""Update the state of the entity."""
if self._process_updates is None:
self._process_updates = asyncio.Lock()
if self._process_updates.locked():
LOGGER.warning(
"Updating Command Line Cover %s took longer than the scheduled update interval %s",
self.name,
self._scan_interval,
)
return
async with self._process_updates:
await self._async_update()
async def _async_update(self) -> None:
"""Update device state."""
if self._command_state:
payload = str(await self.hass.async_add_executor_job(self._query_state))
@ -181,15 +218,19 @@ class CommandCover(CoverEntity):
self._state = None
if payload:
self._state = int(payload)
await self.async_update_ha_state(True)
def open_cover(self, **kwargs: Any) -> None:
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
self._move_cover(self._command_open)
await self.hass.async_add_executor_job(self._move_cover, self._command_open)
await self._update_entity_state(None)
def close_cover(self, **kwargs: Any) -> None:
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close the cover."""
self._move_cover(self._command_close)
await self.hass.async_add_executor_job(self._move_cover, self._command_close)
await self._update_entity_state(None)
def stop_cover(self, **kwargs: Any) -> None:
async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop the cover."""
self._move_cover(self._command_stop)
await self.hass.async_add_executor_job(self._move_cover, self._command_stop)
await self._update_entity_state(None)

View File

@ -1,10 +1,10 @@
"""Allows to configure custom shell commands to turn a value for a sensor."""
from __future__ import annotations
import asyncio
from collections.abc import Mapping
from datetime import timedelta
import json
import logging
import voluptuous as vol
@ -20,6 +20,7 @@ from homeassistant.const import (
CONF_COMMAND,
CONF_DEVICE_CLASS,
CONF_NAME,
CONF_SCAN_INTERVAL,
CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE_TEMPLATE,
@ -28,15 +29,14 @@ 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.event import async_track_time_interval
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
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
from .utils import check_output_or_log
_LOGGER = logging.getLogger(__name__)
CONF_JSON_ATTRIBUTES = "json_attributes"
DEFAULT_NAME = "Command Sensor"
@ -88,6 +88,7 @@ async def async_setup_platform(
if value_template is not None:
value_template.hass = hass
json_attributes: list[str] | None = sensor_config.get(CONF_JSON_ATTRIBUTES)
scan_interval: timedelta = sensor_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
data = CommandSensorData(hass, command, command_timeout)
async_add_entities(
@ -99,15 +100,17 @@ async def async_setup_platform(
value_template,
json_attributes,
unique_id,
scan_interval,
)
],
True,
]
)
class CommandSensor(SensorEntity):
"""Representation of a sensor that is using shell commands."""
_attr_should_poll = False
def __init__(
self,
data: CommandSensorData,
@ -116,6 +119,7 @@ class CommandSensor(SensorEntity):
value_template: Template | None,
json_attributes: list[str] | None,
unique_id: str | None,
scan_interval: timedelta,
) -> None:
"""Initialize the sensor."""
self._attr_name = name
@ -126,8 +130,39 @@ class CommandSensor(SensorEntity):
self._value_template = value_template
self._attr_native_unit_of_measurement = unit_of_measurement
self._attr_unique_id = unique_id
self._scan_interval = scan_interval
self._process_updates: asyncio.Lock | None = None
async def async_update(self) -> None:
async def async_added_to_hass(self) -> None:
"""Call when entity about to be added to hass."""
await super().async_added_to_hass()
await self._update_entity_state(None)
self.async_on_remove(
async_track_time_interval(
self.hass,
self._update_entity_state,
self._scan_interval,
name=f"Command Line Sensor - {self.name}",
cancel_on_shutdown=True,
),
)
async def _update_entity_state(self, now) -> None:
"""Update the state of the entity."""
if self._process_updates is None:
self._process_updates = asyncio.Lock()
if self._process_updates.locked():
LOGGER.warning(
"Updating Command Line Sensor %s took longer than the scheduled update interval %s",
self.name,
self._scan_interval,
)
return
async with self._process_updates:
await self._async_update()
async def _async_update(self) -> None:
"""Get the latest data and updates the state."""
await self.hass.async_add_executor_job(self.data.update)
value = self.data.value
@ -144,11 +179,11 @@ class CommandSensor(SensorEntity):
if k in json_dict
}
else:
_LOGGER.warning("JSON result was not a dictionary")
LOGGER.warning("JSON result was not a dictionary")
except ValueError:
_LOGGER.warning("Unable to parse output as JSON: %s", value)
LOGGER.warning("Unable to parse output as JSON: %s", value)
else:
_LOGGER.warning("Empty reply found when expecting JSON data")
LOGGER.warning("Empty reply found when expecting JSON data")
if self._value_template is None:
self._attr_native_value = None
return
@ -163,6 +198,8 @@ class CommandSensor(SensorEntity):
else:
self._attr_native_value = value
self.async_write_ha_state()
class CommandSensorData:
"""The class for handling the data retrieval."""
@ -191,7 +228,7 @@ class CommandSensorData:
args_to_render = {"arguments": args}
rendered_args = args_compiled.render(args_to_render)
except TemplateError as ex:
_LOGGER.exception("Error rendering command template: %s", ex)
LOGGER.exception("Error rendering command template: %s", ex)
return
else:
rendered_args = None
@ -203,5 +240,5 @@ class CommandSensorData:
# Template used. Construct the string used in the shell
command = f"{prog} {rendered_args}"
_LOGGER.debug("Running command: %s", command)
LOGGER.debug("Running command: %s", command)
self.value = check_output_or_log(command, self.timeout)

View File

@ -1,7 +1,8 @@
"""Support for custom shell commands to turn a switch on/off."""
from __future__ import annotations
import logging
import asyncio
from datetime import timedelta
from typing import TYPE_CHECKING, Any
import voluptuous as vol
@ -20,6 +21,7 @@ from homeassistant.const import (
CONF_ICON,
CONF_ICON_TEMPLATE,
CONF_NAME,
CONF_SCAN_INTERVAL,
CONF_SWITCHES,
CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE,
@ -27,16 +29,17 @@ 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.event import async_track_time_interval
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
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
from .utils import call_shell_with_timeout, check_output_or_log
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=30)
SWITCH_SCHEMA = vol.Schema(
{
@ -112,11 +115,12 @@ async def async_setup_platform(
device_config.get(CONF_COMMAND_STATE),
value_template,
device_config[CONF_COMMAND_TIMEOUT],
device_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL),
)
)
if not switches:
_LOGGER.error("No switches added")
LOGGER.error("No switches added")
return
async_add_entities(switches)
@ -125,6 +129,8 @@ async def async_setup_platform(
class CommandSwitch(ManualTriggerEntity, SwitchEntity):
"""Representation a switch that can be toggled using shell commands."""
_attr_should_poll = False
def __init__(
self,
config: ConfigType,
@ -134,6 +140,7 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
command_state: str | None,
value_template: Template | None,
timeout: int,
scan_interval: timedelta,
) -> None:
"""Initialize the switch."""
super().__init__(self.hass, config)
@ -144,11 +151,26 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
self._command_state = command_state
self._value_template = value_template
self._timeout = timeout
self._attr_should_poll = bool(command_state)
self._scan_interval = scan_interval
self._process_updates: asyncio.Lock | None = None
async def async_added_to_hass(self) -> None:
"""Call when entity about to be added to hass."""
await super().async_added_to_hass()
if self._command_state:
self.async_on_remove(
async_track_time_interval(
self.hass,
self._update_entity_state,
self._scan_interval,
name=f"Command Line Cover - {self.name}",
cancel_on_shutdown=True,
),
)
async def _switch(self, command: str) -> bool:
"""Execute the actual commands."""
_LOGGER.info("Running command: %s", command)
LOGGER.info("Running command: %s", command)
success = (
await self.hass.async_add_executor_job(
@ -158,18 +180,18 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
)
if not success:
_LOGGER.error("Command failed: %s", command)
LOGGER.error("Command failed: %s", command)
return success
def _query_state_value(self, command: str) -> str | None:
"""Execute state command for return value."""
_LOGGER.info("Running state value command: %s", command)
LOGGER.info("Running state value command: %s", command)
return check_output_or_log(command, self._timeout)
def _query_state_code(self, command: str) -> bool:
"""Execute state command for return code."""
_LOGGER.info("Running state code command: %s", command)
LOGGER.info("Running state code command: %s", command)
return (
call_shell_with_timeout(command, self._timeout, log_return_code=False) == 0
)
@ -188,7 +210,22 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
if TYPE_CHECKING:
return None
async def async_update(self) -> None:
async def _update_entity_state(self, now) -> None:
"""Update the state of the entity."""
if self._process_updates is None:
self._process_updates = asyncio.Lock()
if self._process_updates.locked():
LOGGER.warning(
"Updating Command Line Switch %s took longer than the scheduled update interval %s",
self.name,
self._scan_interval,
)
return
async with self._process_updates:
await self._async_update()
async def _async_update(self) -> None:
"""Update device state."""
if self._command_state:
payload = str(await self.hass.async_add_executor_job(self._query_state))
@ -201,15 +238,18 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
if payload or value:
self._attr_is_on = (value or payload).lower() == "true"
self._process_manual_data(payload)
await self.async_update_ha_state(True)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
if await self._switch(self._command_on) and not self._command_state:
self._attr_is_on = True
self.async_schedule_update_ha_state()
await self._update_entity_state(None)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
if await self._switch(self._command_off) and not self._command_state:
self._attr_is_on = False
self.async_schedule_update_ha_state()
await self._update_entity_state(None)

View File

@ -1,17 +1,24 @@
"""The tests for the Command line Binary sensor platform."""
from __future__ import annotations
import asyncio
from datetime import timedelta
from typing import Any
from unittest.mock import patch
import pytest
from homeassistant import setup
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.command_line.binary_sensor import CommandBinarySensor
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
from homeassistant.util import dt as dt_util
from tests.common import async_fire_time_changed
async def test_setup_platform_yaml(hass: HomeAssistant) -> None:
@ -189,3 +196,59 @@ async def test_return_code(
)
await hass.async_block_till_done()
assert "return code 33" in caplog.text
async def test_updating_to_often(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test handling updating when command already running."""
called = []
class MockCommandBinarySensor(CommandBinarySensor):
"""Mock entity that updates slow."""
async def _async_update(self) -> None:
"""Update slow."""
called.append(1)
# Add waiting time
await asyncio.sleep(1)
with patch(
"homeassistant.components.command_line.binary_sensor.CommandBinarySensor",
side_effect=MockCommandBinarySensor,
):
await setup.async_setup_component(
hass,
DOMAIN,
{
"command_line": [
{
"binary_sensor": {
"name": "Test",
"command": "echo 1",
"payload_on": "1",
"payload_off": "0",
"scan_interval": 0.1,
}
}
]
},
)
await hass.async_block_till_done()
assert len(called) == 1
assert (
"Updating Command Line Binary Sensor Test took longer than the scheduled update interval"
not in caplog.text
)
async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=1))
await hass.async_block_till_done()
assert len(called) == 2
assert (
"Updating Command Line Binary Sensor Test took longer than the scheduled update interval"
in caplog.text
)
await asyncio.sleep(0.2)

View File

@ -1,6 +1,8 @@
"""The tests the cover command line platform."""
from __future__ import annotations
import asyncio
from datetime import timedelta
import os
import tempfile
from unittest.mock import patch
@ -9,6 +11,7 @@ import pytest
from homeassistant import config as hass_config, setup
from homeassistant.components.command_line import DOMAIN
from homeassistant.components.command_line.cover import CommandCover
from homeassistant.components.cover import DOMAIN as COVER_DOMAIN, SCAN_INTERVAL
from homeassistant.const import (
ATTR_ENTITY_ID,
@ -320,3 +323,58 @@ async def test_unique_id(
assert entity_registry.async_get_entity_id(
"cover", "command_line", "not-so-unique-anymore"
)
async def test_updating_to_often(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test handling updating when command already running."""
called = []
class MockCommandCover(CommandCover):
"""Mock entity that updates slow."""
async def _async_update(self) -> None:
"""Update slow."""
called.append(1)
# Add waiting time
await asyncio.sleep(1)
with patch(
"homeassistant.components.command_line.cover.CommandCover",
side_effect=MockCommandCover,
):
await setup.async_setup_component(
hass,
DOMAIN,
{
"command_line": [
{
"cover": {
"command_state": "echo 1",
"value_template": "{{ value }}",
"name": "Test",
"scan_interval": 0.1,
}
}
]
},
)
await hass.async_block_till_done()
assert len(called) == 0
assert (
"Updating Command Line Cover Test took longer than the scheduled update interval"
not in caplog.text
)
async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=1))
await hass.async_block_till_done()
assert len(called) == 1
assert (
"Updating Command Line Cover Test took longer than the scheduled update interval"
in caplog.text
)
await asyncio.sleep(0.2)

View File

@ -1,6 +1,7 @@
"""The tests for the Command line sensor platform."""
from __future__ import annotations
import asyncio
from datetime import timedelta
from typing import Any
from unittest.mock import patch
@ -9,6 +10,7 @@ import pytest
from homeassistant import setup
from homeassistant.components.command_line import DOMAIN
from homeassistant.components.command_line.sensor import CommandSensor
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@ -530,3 +532,57 @@ async def test_unique_id(
assert entity_registry.async_get_entity_id(
"sensor", "command_line", "not-so-unique-anymore"
)
async def test_updating_to_often(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test handling updating when command already running."""
called = []
class MockCommandSensor(CommandSensor):
"""Mock entity that updates slow."""
async def _async_update(self) -> None:
"""Update slow."""
called.append(1)
# Add waiting time
await asyncio.sleep(1)
with patch(
"homeassistant.components.command_line.sensor.CommandSensor",
side_effect=MockCommandSensor,
):
await setup.async_setup_component(
hass,
DOMAIN,
{
"command_line": [
{
"sensor": {
"name": "Test",
"command": "echo 1",
"scan_interval": 0.1,
}
}
]
},
)
await hass.async_block_till_done()
assert len(called) == 1
assert (
"Updating Command Line Sensor Test took longer than the scheduled update interval"
not in caplog.text
)
async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=1))
await hass.async_block_till_done()
assert len(called) == 2
assert (
"Updating Command Line Sensor Test took longer than the scheduled update interval"
in caplog.text
)
await asyncio.sleep(0.2)

View File

@ -1,6 +1,8 @@
"""The tests for the Command line switch platform."""
from __future__ import annotations
import asyncio
from datetime import timedelta
import json
import os
import subprocess
@ -11,6 +13,7 @@ import pytest
from homeassistant import setup
from homeassistant.components.command_line import DOMAIN
from homeassistant.components.command_line.switch import CommandSwitch
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SCAN_INTERVAL
from homeassistant.const import (
ATTR_ENTITY_ID,
@ -637,3 +640,59 @@ async def test_templating(hass: HomeAssistant) -> None:
assert entity_state.attributes.get("icon") == "mdi:on"
assert entity_state2.state == STATE_ON
assert entity_state2.attributes.get("icon") == "mdi:on"
async def test_updating_to_often(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test handling updating when command already running."""
called = []
class MockCommandSwitch(CommandSwitch):
"""Mock entity that updates slow."""
async def _async_update(self) -> None:
"""Update slow."""
called.append(1)
# Add waiting time
await asyncio.sleep(1)
with patch(
"homeassistant.components.command_line.switch.CommandSwitch",
side_effect=MockCommandSwitch,
):
await setup.async_setup_component(
hass,
DOMAIN,
{
"command_line": [
{
"switch": {
"command_state": "echo 1",
"command_on": "echo 2",
"command_off": "echo 3",
"name": "Test",
"scan_interval": 0.1,
}
}
]
},
)
await hass.async_block_till_done()
assert len(called) == 0
assert (
"Updating Command Line Switch Test took longer than the scheduled update interval"
not in caplog.text
)
async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=1))
await hass.async_block_till_done()
assert len(called) == 1
assert (
"Updating Command Line Switch Test took longer than the scheduled update interval"
in caplog.text
)
await asyncio.sleep(0.2)