Skip to content

Commit

Permalink
Add scan interval to Command Line (#93752)
Browse files Browse the repository at this point in the history
* Add scan interval

* Handle previous not complete

* Fix faulty text

* Add tests

* lingering

* Cool down

* Fix tests
  • Loading branch information
gjohansson-ST authored and balloob committed Jun 3, 2023
1 parent 32f7f39 commit 6ff55a6
Show file tree
Hide file tree
Showing 10 changed files with 463 additions and 41 deletions.
25 changes: 23 additions & 2 deletions homeassistant/components/command_line/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,24 @@
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,
Expand All @@ -34,6 +42,7 @@
CONF_NAME,
CONF_PAYLOAD_OFF,
CONF_PAYLOAD_ON,
CONF_SCAN_INTERVAL,
CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE_TEMPLATE,
Expand Down Expand Up @@ -74,6 +83,9 @@
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(
Expand All @@ -86,6 +98,9 @@
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(
Expand All @@ -106,6 +121,9 @@
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(
Expand All @@ -118,6 +136,9 @@
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(
Expand Down
47 changes: 45 additions & 2 deletions homeassistant/components/command_line/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -18,17 +19,19 @@
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"
Expand Down Expand Up @@ -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)
Expand All @@ -98,6 +104,7 @@ async def async_setup_platform(
payload_off,
value_template,
unique_id,
scan_interval,
)
],
True,
Expand All @@ -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,
Expand All @@ -116,6 +125,7 @@ def __init__(
payload_off: str,
value_template: Template | None,
unique_id: str | None,
scan_interval: timedelta,
) -> None:
"""Initialize the Command line binary sensor."""
self.data = data
Expand All @@ -126,8 +136,39 @@ def __init__(
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_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 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 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
Expand All @@ -141,3 +182,5 @@ async def async_update(self) -> None:
self._attr_is_on = True
elif value == self._payload_off:
self._attr_is_on = False

self.async_write_ha_state()
4 changes: 4 additions & 0 deletions homeassistant/components/command_line/const.py
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
71 changes: 56 additions & 15 deletions homeassistant/components/command_line/cover.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -19,21 +20,23 @@
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(
{
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand All @@ -120,6 +126,7 @@ def __init__(
value_template: Template | None,
timeout: int,
unique_id: str | None,
scan_interval: timedelta,
) -> None:
"""Initialize the cover."""
self._attr_name = name
Expand All @@ -131,17 +138,32 @@ def __init__(
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
)

Expand All @@ -165,12 +187,27 @@ def current_cover_position(self) -> int | None:
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))
Expand All @@ -181,15 +218,19 @@ async def async_update(self) -> None:
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)
Loading

0 comments on commit 6ff55a6

Please sign in to comment.