mirror of
https://github.com/home-assistant/core.git
synced 2025-06-18 03:57:10 +00:00
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:
parent
aa636a2805
commit
038b0e6d23
@ -11,16 +11,24 @@ import voluptuous as vol
|
|||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
|
DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
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.notify import DOMAIN as NOTIFY_DOMAIN
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
CONF_STATE_CLASS,
|
CONF_STATE_CLASS,
|
||||||
DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA,
|
DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||||
DOMAIN as SENSOR_DOMAIN,
|
DOMAIN as SENSOR_DOMAIN,
|
||||||
|
SCAN_INTERVAL as SENSOR_DEFAULT_SCAN_INTERVAL,
|
||||||
STATE_CLASSES_SCHEMA as SENSOR_STATE_CLASSES_SCHEMA,
|
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 (
|
from homeassistant.const import (
|
||||||
CONF_COMMAND,
|
CONF_COMMAND,
|
||||||
CONF_COMMAND_CLOSE,
|
CONF_COMMAND_CLOSE,
|
||||||
@ -34,6 +42,7 @@ from homeassistant.const import (
|
|||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_PAYLOAD_OFF,
|
CONF_PAYLOAD_OFF,
|
||||||
CONF_PAYLOAD_ON,
|
CONF_PAYLOAD_ON,
|
||||||
|
CONF_SCAN_INTERVAL,
|
||||||
CONF_UNIQUE_ID,
|
CONF_UNIQUE_ID,
|
||||||
CONF_UNIT_OF_MEASUREMENT,
|
CONF_UNIT_OF_MEASUREMENT,
|
||||||
CONF_VALUE_TEMPLATE,
|
CONF_VALUE_TEMPLATE,
|
||||||
@ -74,6 +83,9 @@ BINARY_SENSOR_SCHEMA = vol.Schema(
|
|||||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||||
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
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(
|
COVER_SCHEMA = vol.Schema(
|
||||||
@ -86,6 +98,9 @@ COVER_SCHEMA = vol.Schema(
|
|||||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||||
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
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(
|
NOTIFY_SCHEMA = vol.Schema(
|
||||||
@ -106,6 +121,9 @@ SENSOR_SCHEMA = vol.Schema(
|
|||||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||||
vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
|
vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||||
vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_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(
|
SWITCH_SCHEMA = vol.Schema(
|
||||||
@ -118,6 +136,9 @@ SWITCH_SCHEMA = vol.Schema(
|
|||||||
vol.Optional(CONF_ICON): cv.template,
|
vol.Optional(CONF_ICON): cv.template,
|
||||||
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
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(
|
COMBINED_SCHEMA = vol.Schema(
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Support for custom shell commands to retrieve values."""
|
"""Support for custom shell commands to retrieve values."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@ -18,17 +19,19 @@ from homeassistant.const import (
|
|||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_PAYLOAD_OFF,
|
CONF_PAYLOAD_OFF,
|
||||||
CONF_PAYLOAD_ON,
|
CONF_PAYLOAD_ON,
|
||||||
|
CONF_SCAN_INTERVAL,
|
||||||
CONF_UNIQUE_ID,
|
CONF_UNIQUE_ID,
|
||||||
CONF_VALUE_TEMPLATE,
|
CONF_VALUE_TEMPLATE,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
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.issue_registry import IssueSeverity, async_create_issue
|
||||||
from homeassistant.helpers.template import Template
|
from homeassistant.helpers.template import Template
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
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
|
from .sensor import CommandSensorData
|
||||||
|
|
||||||
DEFAULT_NAME = "Binary Command Sensor"
|
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)
|
value_template: Template | None = binary_sensor_config.get(CONF_VALUE_TEMPLATE)
|
||||||
command_timeout: int = binary_sensor_config[CONF_COMMAND_TIMEOUT]
|
command_timeout: int = binary_sensor_config[CONF_COMMAND_TIMEOUT]
|
||||||
unique_id: str | None = binary_sensor_config.get(CONF_UNIQUE_ID)
|
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:
|
if value_template is not None:
|
||||||
value_template.hass = hass
|
value_template.hass = hass
|
||||||
data = CommandSensorData(hass, command, command_timeout)
|
data = CommandSensorData(hass, command, command_timeout)
|
||||||
@ -98,6 +104,7 @@ async def async_setup_platform(
|
|||||||
payload_off,
|
payload_off,
|
||||||
value_template,
|
value_template,
|
||||||
unique_id,
|
unique_id,
|
||||||
|
scan_interval,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
True,
|
True,
|
||||||
@ -107,6 +114,8 @@ async def async_setup_platform(
|
|||||||
class CommandBinarySensor(BinarySensorEntity):
|
class CommandBinarySensor(BinarySensorEntity):
|
||||||
"""Representation of a command line binary sensor."""
|
"""Representation of a command line binary sensor."""
|
||||||
|
|
||||||
|
_attr_should_poll = False
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
data: CommandSensorData,
|
data: CommandSensorData,
|
||||||
@ -116,6 +125,7 @@ class CommandBinarySensor(BinarySensorEntity):
|
|||||||
payload_off: str,
|
payload_off: str,
|
||||||
value_template: Template | None,
|
value_template: Template | None,
|
||||||
unique_id: str | None,
|
unique_id: str | None,
|
||||||
|
scan_interval: timedelta,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the Command line binary sensor."""
|
"""Initialize the Command line binary sensor."""
|
||||||
self.data = data
|
self.data = data
|
||||||
@ -126,8 +136,39 @@ class CommandBinarySensor(BinarySensorEntity):
|
|||||||
self._payload_off = payload_off
|
self._payload_off = payload_off
|
||||||
self._value_template = value_template
|
self._value_template = value_template
|
||||||
self._attr_unique_id = unique_id
|
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."""
|
"""Get the latest data and updates the state."""
|
||||||
await self.hass.async_add_executor_job(self.data.update)
|
await self.hass.async_add_executor_job(self.data.update)
|
||||||
value = self.data.value
|
value = self.data.value
|
||||||
@ -141,3 +182,5 @@ class CommandBinarySensor(BinarySensorEntity):
|
|||||||
self._attr_is_on = True
|
self._attr_is_on = True
|
||||||
elif value == self._payload_off:
|
elif value == self._payload_off:
|
||||||
self._attr_is_on = False
|
self._attr_is_on = False
|
||||||
|
|
||||||
|
self.async_write_ha_state()
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
"""Allows to configure custom shell commands to turn a value for a sensor."""
|
"""Allows to configure custom shell commands to turn a value for a sensor."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(__package__)
|
||||||
|
|
||||||
CONF_COMMAND_TIMEOUT = "command_timeout"
|
CONF_COMMAND_TIMEOUT = "command_timeout"
|
||||||
DEFAULT_TIMEOUT = 15
|
DEFAULT_TIMEOUT = 15
|
||||||
DOMAIN = "command_line"
|
DOMAIN = "command_line"
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
"""Support for command line covers."""
|
"""Support for command line covers."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import asyncio
|
||||||
|
from datetime import timedelta
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@ -19,21 +20,23 @@ from homeassistant.const import (
|
|||||||
CONF_COVERS,
|
CONF_COVERS,
|
||||||
CONF_FRIENDLY_NAME,
|
CONF_FRIENDLY_NAME,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
|
CONF_SCAN_INTERVAL,
|
||||||
CONF_UNIQUE_ID,
|
CONF_UNIQUE_ID,
|
||||||
CONF_VALUE_TEMPLATE,
|
CONF_VALUE_TEMPLATE,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
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.issue_registry import IssueSeverity, async_create_issue
|
||||||
from homeassistant.helpers.template import Template
|
from homeassistant.helpers.template import Template
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
from homeassistant.util import slugify
|
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
|
from .utils import call_shell_with_timeout, check_output_or_log
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
SCAN_INTERVAL = timedelta(seconds=15)
|
||||||
|
|
||||||
COVER_SCHEMA = vol.Schema(
|
COVER_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
@ -97,11 +100,12 @@ async def async_setup_platform(
|
|||||||
value_template,
|
value_template,
|
||||||
device_config[CONF_COMMAND_TIMEOUT],
|
device_config[CONF_COMMAND_TIMEOUT],
|
||||||
device_config.get(CONF_UNIQUE_ID),
|
device_config.get(CONF_UNIQUE_ID),
|
||||||
|
device_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if not covers:
|
if not covers:
|
||||||
_LOGGER.error("No covers added")
|
LOGGER.error("No covers added")
|
||||||
return
|
return
|
||||||
|
|
||||||
async_add_entities(covers)
|
async_add_entities(covers)
|
||||||
@ -110,6 +114,8 @@ async def async_setup_platform(
|
|||||||
class CommandCover(CoverEntity):
|
class CommandCover(CoverEntity):
|
||||||
"""Representation a command line cover."""
|
"""Representation a command line cover."""
|
||||||
|
|
||||||
|
_attr_should_poll = False
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
@ -120,6 +126,7 @@ class CommandCover(CoverEntity):
|
|||||||
value_template: Template | None,
|
value_template: Template | None,
|
||||||
timeout: int,
|
timeout: int,
|
||||||
unique_id: str | None,
|
unique_id: str | None,
|
||||||
|
scan_interval: timedelta,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the cover."""
|
"""Initialize the cover."""
|
||||||
self._attr_name = name
|
self._attr_name = name
|
||||||
@ -131,17 +138,32 @@ class CommandCover(CoverEntity):
|
|||||||
self._value_template = value_template
|
self._value_template = value_template
|
||||||
self._timeout = timeout
|
self._timeout = timeout
|
||||||
self._attr_unique_id = unique_id
|
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:
|
def _move_cover(self, command: str) -> bool:
|
||||||
"""Execute the actual commands."""
|
"""Execute the actual commands."""
|
||||||
_LOGGER.info("Running command: %s", command)
|
LOGGER.info("Running command: %s", command)
|
||||||
|
|
||||||
returncode = call_shell_with_timeout(command, self._timeout)
|
returncode = call_shell_with_timeout(command, self._timeout)
|
||||||
success = returncode == 0
|
success = returncode == 0
|
||||||
|
|
||||||
if not success:
|
if not success:
|
||||||
_LOGGER.error(
|
LOGGER.error(
|
||||||
"Command failed (with return code %s): %s", returncode, command
|
"Command failed (with return code %s): %s", returncode, command
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -165,12 +187,27 @@ class CommandCover(CoverEntity):
|
|||||||
def _query_state(self) -> str | None:
|
def _query_state(self) -> str | None:
|
||||||
"""Query for the state."""
|
"""Query for the state."""
|
||||||
if self._command_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)
|
return check_output_or_log(self._command_state, self._timeout)
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
return None
|
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."""
|
"""Update device state."""
|
||||||
if self._command_state:
|
if self._command_state:
|
||||||
payload = str(await self.hass.async_add_executor_job(self._query_state))
|
payload = str(await self.hass.async_add_executor_job(self._query_state))
|
||||||
@ -181,15 +218,19 @@ class CommandCover(CoverEntity):
|
|||||||
self._state = None
|
self._state = None
|
||||||
if payload:
|
if payload:
|
||||||
self._state = int(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."""
|
"""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."""
|
"""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."""
|
"""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)
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
"""Allows to configure custom shell commands to turn a value for a sensor."""
|
"""Allows to configure custom shell commands to turn a value for a sensor."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import json
|
import json
|
||||||
import logging
|
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@ -20,6 +20,7 @@ from homeassistant.const import (
|
|||||||
CONF_COMMAND,
|
CONF_COMMAND,
|
||||||
CONF_DEVICE_CLASS,
|
CONF_DEVICE_CLASS,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
|
CONF_SCAN_INTERVAL,
|
||||||
CONF_UNIQUE_ID,
|
CONF_UNIQUE_ID,
|
||||||
CONF_UNIT_OF_MEASUREMENT,
|
CONF_UNIT_OF_MEASUREMENT,
|
||||||
CONF_VALUE_TEMPLATE,
|
CONF_VALUE_TEMPLATE,
|
||||||
@ -28,15 +29,14 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.exceptions import TemplateError
|
from homeassistant.exceptions import TemplateError
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
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.issue_registry import IssueSeverity, async_create_issue
|
||||||
from homeassistant.helpers.template import Template
|
from homeassistant.helpers.template import Template
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
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
|
from .utils import check_output_or_log
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
CONF_JSON_ATTRIBUTES = "json_attributes"
|
CONF_JSON_ATTRIBUTES = "json_attributes"
|
||||||
|
|
||||||
DEFAULT_NAME = "Command Sensor"
|
DEFAULT_NAME = "Command Sensor"
|
||||||
@ -88,6 +88,7 @@ async def async_setup_platform(
|
|||||||
if value_template is not None:
|
if value_template is not None:
|
||||||
value_template.hass = hass
|
value_template.hass = hass
|
||||||
json_attributes: list[str] | None = sensor_config.get(CONF_JSON_ATTRIBUTES)
|
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)
|
data = CommandSensorData(hass, command, command_timeout)
|
||||||
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
@ -99,15 +100,17 @@ async def async_setup_platform(
|
|||||||
value_template,
|
value_template,
|
||||||
json_attributes,
|
json_attributes,
|
||||||
unique_id,
|
unique_id,
|
||||||
|
scan_interval,
|
||||||
)
|
)
|
||||||
],
|
]
|
||||||
True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CommandSensor(SensorEntity):
|
class CommandSensor(SensorEntity):
|
||||||
"""Representation of a sensor that is using shell commands."""
|
"""Representation of a sensor that is using shell commands."""
|
||||||
|
|
||||||
|
_attr_should_poll = False
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
data: CommandSensorData,
|
data: CommandSensorData,
|
||||||
@ -116,6 +119,7 @@ class CommandSensor(SensorEntity):
|
|||||||
value_template: Template | None,
|
value_template: Template | None,
|
||||||
json_attributes: list[str] | None,
|
json_attributes: list[str] | None,
|
||||||
unique_id: str | None,
|
unique_id: str | None,
|
||||||
|
scan_interval: timedelta,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
self._attr_name = name
|
self._attr_name = name
|
||||||
@ -126,8 +130,39 @@ class CommandSensor(SensorEntity):
|
|||||||
self._value_template = value_template
|
self._value_template = value_template
|
||||||
self._attr_native_unit_of_measurement = unit_of_measurement
|
self._attr_native_unit_of_measurement = unit_of_measurement
|
||||||
self._attr_unique_id = unique_id
|
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."""
|
"""Get the latest data and updates the state."""
|
||||||
await self.hass.async_add_executor_job(self.data.update)
|
await self.hass.async_add_executor_job(self.data.update)
|
||||||
value = self.data.value
|
value = self.data.value
|
||||||
@ -144,11 +179,11 @@ class CommandSensor(SensorEntity):
|
|||||||
if k in json_dict
|
if k in json_dict
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
_LOGGER.warning("JSON result was not a dictionary")
|
LOGGER.warning("JSON result was not a dictionary")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
_LOGGER.warning("Unable to parse output as JSON: %s", value)
|
LOGGER.warning("Unable to parse output as JSON: %s", value)
|
||||||
else:
|
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:
|
if self._value_template is None:
|
||||||
self._attr_native_value = None
|
self._attr_native_value = None
|
||||||
return
|
return
|
||||||
@ -163,6 +198,8 @@ class CommandSensor(SensorEntity):
|
|||||||
else:
|
else:
|
||||||
self._attr_native_value = value
|
self._attr_native_value = value
|
||||||
|
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
||||||
class CommandSensorData:
|
class CommandSensorData:
|
||||||
"""The class for handling the data retrieval."""
|
"""The class for handling the data retrieval."""
|
||||||
@ -191,7 +228,7 @@ class CommandSensorData:
|
|||||||
args_to_render = {"arguments": args}
|
args_to_render = {"arguments": args}
|
||||||
rendered_args = args_compiled.render(args_to_render)
|
rendered_args = args_compiled.render(args_to_render)
|
||||||
except TemplateError as ex:
|
except TemplateError as ex:
|
||||||
_LOGGER.exception("Error rendering command template: %s", ex)
|
LOGGER.exception("Error rendering command template: %s", ex)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
rendered_args = None
|
rendered_args = None
|
||||||
@ -203,5 +240,5 @@ class CommandSensorData:
|
|||||||
# Template used. Construct the string used in the shell
|
# Template used. Construct the string used in the shell
|
||||||
command = f"{prog} {rendered_args}"
|
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)
|
self.value = check_output_or_log(command, self.timeout)
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
"""Support for custom shell commands to turn a switch on/off."""
|
"""Support for custom shell commands to turn a switch on/off."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import asyncio
|
||||||
|
from datetime import timedelta
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@ -20,6 +21,7 @@ from homeassistant.const import (
|
|||||||
CONF_ICON,
|
CONF_ICON,
|
||||||
CONF_ICON_TEMPLATE,
|
CONF_ICON_TEMPLATE,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
|
CONF_SCAN_INTERVAL,
|
||||||
CONF_SWITCHES,
|
CONF_SWITCHES,
|
||||||
CONF_UNIQUE_ID,
|
CONF_UNIQUE_ID,
|
||||||
CONF_VALUE_TEMPLATE,
|
CONF_VALUE_TEMPLATE,
|
||||||
@ -27,16 +29,17 @@ from homeassistant.const import (
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
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.issue_registry import IssueSeverity, async_create_issue
|
||||||
from homeassistant.helpers.template import Template
|
from homeassistant.helpers.template import Template
|
||||||
from homeassistant.helpers.template_entity import ManualTriggerEntity
|
from homeassistant.helpers.template_entity import ManualTriggerEntity
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
from homeassistant.util import slugify
|
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
|
from .utils import call_shell_with_timeout, check_output_or_log
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
SCAN_INTERVAL = timedelta(seconds=30)
|
||||||
|
|
||||||
SWITCH_SCHEMA = vol.Schema(
|
SWITCH_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
@ -112,11 +115,12 @@ async def async_setup_platform(
|
|||||||
device_config.get(CONF_COMMAND_STATE),
|
device_config.get(CONF_COMMAND_STATE),
|
||||||
value_template,
|
value_template,
|
||||||
device_config[CONF_COMMAND_TIMEOUT],
|
device_config[CONF_COMMAND_TIMEOUT],
|
||||||
|
device_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if not switches:
|
if not switches:
|
||||||
_LOGGER.error("No switches added")
|
LOGGER.error("No switches added")
|
||||||
return
|
return
|
||||||
|
|
||||||
async_add_entities(switches)
|
async_add_entities(switches)
|
||||||
@ -125,6 +129,8 @@ async def async_setup_platform(
|
|||||||
class CommandSwitch(ManualTriggerEntity, SwitchEntity):
|
class CommandSwitch(ManualTriggerEntity, SwitchEntity):
|
||||||
"""Representation a switch that can be toggled using shell commands."""
|
"""Representation a switch that can be toggled using shell commands."""
|
||||||
|
|
||||||
|
_attr_should_poll = False
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
config: ConfigType,
|
config: ConfigType,
|
||||||
@ -134,6 +140,7 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
|
|||||||
command_state: str | None,
|
command_state: str | None,
|
||||||
value_template: Template | None,
|
value_template: Template | None,
|
||||||
timeout: int,
|
timeout: int,
|
||||||
|
scan_interval: timedelta,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the switch."""
|
"""Initialize the switch."""
|
||||||
super().__init__(self.hass, config)
|
super().__init__(self.hass, config)
|
||||||
@ -144,11 +151,26 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
|
|||||||
self._command_state = command_state
|
self._command_state = command_state
|
||||||
self._value_template = value_template
|
self._value_template = value_template
|
||||||
self._timeout = timeout
|
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:
|
async def _switch(self, command: str) -> bool:
|
||||||
"""Execute the actual commands."""
|
"""Execute the actual commands."""
|
||||||
_LOGGER.info("Running command: %s", command)
|
LOGGER.info("Running command: %s", command)
|
||||||
|
|
||||||
success = (
|
success = (
|
||||||
await self.hass.async_add_executor_job(
|
await self.hass.async_add_executor_job(
|
||||||
@ -158,18 +180,18 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not success:
|
if not success:
|
||||||
_LOGGER.error("Command failed: %s", command)
|
LOGGER.error("Command failed: %s", command)
|
||||||
|
|
||||||
return success
|
return success
|
||||||
|
|
||||||
def _query_state_value(self, command: str) -> str | None:
|
def _query_state_value(self, command: str) -> str | None:
|
||||||
"""Execute state command for return value."""
|
"""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)
|
return check_output_or_log(command, self._timeout)
|
||||||
|
|
||||||
def _query_state_code(self, command: str) -> bool:
|
def _query_state_code(self, command: str) -> bool:
|
||||||
"""Execute state command for return code."""
|
"""Execute state command for return code."""
|
||||||
_LOGGER.info("Running state code command: %s", command)
|
LOGGER.info("Running state code command: %s", command)
|
||||||
return (
|
return (
|
||||||
call_shell_with_timeout(command, self._timeout, log_return_code=False) == 0
|
call_shell_with_timeout(command, self._timeout, log_return_code=False) == 0
|
||||||
)
|
)
|
||||||
@ -188,7 +210,22 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
return None
|
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."""
|
"""Update device state."""
|
||||||
if self._command_state:
|
if self._command_state:
|
||||||
payload = str(await self.hass.async_add_executor_job(self._query_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:
|
if payload or value:
|
||||||
self._attr_is_on = (value or payload).lower() == "true"
|
self._attr_is_on = (value or payload).lower() == "true"
|
||||||
self._process_manual_data(payload)
|
self._process_manual_data(payload)
|
||||||
|
await self.async_update_ha_state(True)
|
||||||
|
|
||||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
"""Turn the device on."""
|
"""Turn the device on."""
|
||||||
if await self._switch(self._command_on) and not self._command_state:
|
if await self._switch(self._command_on) and not self._command_state:
|
||||||
self._attr_is_on = True
|
self._attr_is_on = True
|
||||||
self.async_schedule_update_ha_state()
|
self.async_schedule_update_ha_state()
|
||||||
|
await self._update_entity_state(None)
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Turn the device off."""
|
"""Turn the device off."""
|
||||||
if await self._switch(self._command_off) and not self._command_state:
|
if await self._switch(self._command_off) and not self._command_state:
|
||||||
self._attr_is_on = False
|
self._attr_is_on = False
|
||||||
self.async_schedule_update_ha_state()
|
self.async_schedule_update_ha_state()
|
||||||
|
await self._update_entity_state(None)
|
||||||
|
@ -1,17 +1,24 @@
|
|||||||
"""The tests for the Command line Binary sensor platform."""
|
"""The tests for the Command line Binary sensor platform."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from datetime import timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant import setup
|
from homeassistant import setup
|
||||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
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.components.command_line.const import DOMAIN
|
||||||
from homeassistant.const import STATE_OFF, STATE_ON
|
from homeassistant.const import STATE_OFF, STATE_ON
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
import homeassistant.helpers.issue_registry as ir
|
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:
|
async def test_setup_platform_yaml(hass: HomeAssistant) -> None:
|
||||||
@ -189,3 +196,59 @@ async def test_return_code(
|
|||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert "return code 33" in caplog.text
|
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)
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
"""The tests the cover command line platform."""
|
"""The tests the cover command line platform."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from datetime import timedelta
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
@ -9,6 +11,7 @@ import pytest
|
|||||||
|
|
||||||
from homeassistant import config as hass_config, setup
|
from homeassistant import config as hass_config, setup
|
||||||
from homeassistant.components.command_line import DOMAIN
|
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.components.cover import DOMAIN as COVER_DOMAIN, SCAN_INTERVAL
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
@ -320,3 +323,58 @@ async def test_unique_id(
|
|||||||
assert entity_registry.async_get_entity_id(
|
assert entity_registry.async_get_entity_id(
|
||||||
"cover", "command_line", "not-so-unique-anymore"
|
"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)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""The tests for the Command line sensor platform."""
|
"""The tests for the Command line sensor platform."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
@ -9,6 +10,7 @@ import pytest
|
|||||||
|
|
||||||
from homeassistant import setup
|
from homeassistant import setup
|
||||||
from homeassistant.components.command_line import DOMAIN
|
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.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
@ -530,3 +532,57 @@ async def test_unique_id(
|
|||||||
assert entity_registry.async_get_entity_id(
|
assert entity_registry.async_get_entity_id(
|
||||||
"sensor", "command_line", "not-so-unique-anymore"
|
"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)
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
"""The tests for the Command line switch platform."""
|
"""The tests for the Command line switch platform."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from datetime import timedelta
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
@ -11,6 +13,7 @@ import pytest
|
|||||||
|
|
||||||
from homeassistant import setup
|
from homeassistant import setup
|
||||||
from homeassistant.components.command_line import DOMAIN
|
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.components.switch import DOMAIN as SWITCH_DOMAIN, SCAN_INTERVAL
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
@ -637,3 +640,59 @@ async def test_templating(hass: HomeAssistant) -> None:
|
|||||||
assert entity_state.attributes.get("icon") == "mdi:on"
|
assert entity_state.attributes.get("icon") == "mdi:on"
|
||||||
assert entity_state2.state == STATE_ON
|
assert entity_state2.state == STATE_ON
|
||||||
assert entity_state2.attributes.get("icon") == "mdi: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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user