Home Assistant Unofficial Reference 2024.12.1
switch.py
Go to the documentation of this file.
1 """Support for custom shell commands to turn a switch on/off."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from datetime import datetime, timedelta
7 from typing import TYPE_CHECKING, Any, cast
8 
9 from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity
10 from homeassistant.const import (
11  CONF_COMMAND_OFF,
12  CONF_COMMAND_ON,
13  CONF_COMMAND_STATE,
14  CONF_NAME,
15  CONF_SCAN_INTERVAL,
16  CONF_VALUE_TEMPLATE,
17 )
18 from homeassistant.core import HomeAssistant
19 from homeassistant.helpers.entity_platform import AddEntitiesCallback
20 from homeassistant.helpers.event import async_track_time_interval
21 from homeassistant.helpers.template import Template
22 from homeassistant.helpers.trigger_template_entity import ManualTriggerEntity
23 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
24 from homeassistant.util import dt as dt_util, slugify
25 
26 from .const import CONF_COMMAND_TIMEOUT, LOGGER, TRIGGER_ENTITY_OPTIONS
27 from .utils import async_call_shell_with_timeout, async_check_output_or_log
28 
29 SCAN_INTERVAL = timedelta(seconds=30)
30 
31 
33  hass: HomeAssistant,
34  config: ConfigType,
35  async_add_entities: AddEntitiesCallback,
36  discovery_info: DiscoveryInfoType | None = None,
37 ) -> None:
38  """Find and return switches controlled by shell commands."""
39  if not discovery_info:
40  return
41 
42  switches = []
43  discovery_info = cast(DiscoveryInfoType, discovery_info)
44  entities: dict[str, dict[str, Any]] = {
45  slugify(discovery_info[CONF_NAME]): discovery_info
46  }
47 
48  for object_id, switch_config in entities.items():
49  trigger_entity_config = {
50  CONF_NAME: Template(switch_config.get(CONF_NAME, object_id), hass),
51  **{k: v for k, v in switch_config.items() if k in TRIGGER_ENTITY_OPTIONS},
52  }
53 
54  switches.append(
56  trigger_entity_config,
57  object_id,
58  switch_config[CONF_COMMAND_ON],
59  switch_config[CONF_COMMAND_OFF],
60  switch_config.get(CONF_COMMAND_STATE),
61  switch_config.get(CONF_VALUE_TEMPLATE),
62  switch_config[CONF_COMMAND_TIMEOUT],
63  switch_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL),
64  )
65  )
66 
67  async_add_entities(switches)
68 
69 
71  """Representation a switch that can be toggled using shell commands."""
72 
73  _attr_should_poll = False
74 
75  def __init__(
76  self,
77  config: ConfigType,
78  object_id: str,
79  command_on: str,
80  command_off: str,
81  command_state: str | None,
82  value_template: Template | None,
83  timeout: int,
84  scan_interval: timedelta,
85  ) -> None:
86  """Initialize the switch."""
87  super().__init__(self.hasshasshass, config)
88  self.entity_identity_identity_id = ENTITY_ID_FORMAT.format(object_id)
89  self._attr_is_on_attr_is_on = False
90  self._command_on_command_on = command_on
91  self._command_off_command_off = command_off
92  self._command_state_command_state = command_state
93  self._value_template_value_template = value_template
94  self._timeout_timeout = timeout
95  self._scan_interval_scan_interval = scan_interval
96  self._process_updates_process_updates: asyncio.Lock | None = None
97 
98  async def async_added_to_hass(self) -> None:
99  """Call when entity about to be added to hass."""
100  await super().async_added_to_hass()
101  if self._command_state_command_state:
102  self.async_on_removeasync_on_remove(
104  self.hasshasshass,
105  self._update_entity_state_update_entity_state,
106  self._scan_interval_scan_interval,
107  name=f"Command Line Cover - {self.name}",
108  cancel_on_shutdown=True,
109  ),
110  )
111 
112  async def _switch(self, command: str) -> bool:
113  """Execute the actual commands."""
114  LOGGER.debug("Running command: %s", command)
115 
116  success = await async_call_shell_with_timeout(command, self._timeout_timeout) == 0
117 
118  if not success:
119  LOGGER.error("Command failed: %s", command)
120 
121  return success
122 
123  async def _async_query_state_value(self, command: str) -> str | None:
124  """Execute state command for return value."""
125  LOGGER.debug("Running state value command: %s", command)
126  return await async_check_output_or_log(command, self._timeout_timeout)
127 
128  async def _async_query_state_code(self, command: str) -> bool:
129  """Execute state command for return code."""
130  LOGGER.debug("Running state code command: %s", command)
131  return (
133  command, self._timeout_timeout, log_return_code=False
134  )
135  == 0
136  )
137 
138  @property
139  def assumed_state(self) -> bool:
140  """Return true if we do optimistic updates."""
141  return self._command_state_command_state is None
142 
143  async def _async_query_state(self) -> str | int | None:
144  """Query for state."""
145  if TYPE_CHECKING:
146  assert self._command_state_command_state
147  if self._value_template_value_template:
148  return await self._async_query_state_value_async_query_state_value(self._command_state_command_state)
149  return await self._async_query_state_code_async_query_state_code(self._command_state_command_state)
150 
151  async def _update_entity_state(self, now: datetime | None = None) -> None:
152  """Update the state of the entity."""
153  if self._process_updates_process_updates is None:
154  self._process_updates_process_updates = asyncio.Lock()
155  if self._process_updates_process_updates.locked():
156  LOGGER.warning(
157  "Updating Command Line Switch %s took longer than the scheduled update interval %s",
158  self.namenamename,
159  self._scan_interval_scan_interval,
160  )
161  return
162 
163  async with self._process_updates_process_updates:
164  await self._async_update_async_update()
165 
166  async def _async_update(self) -> None:
167  """Update device state."""
168  if self._command_state_command_state:
169  payload = str(await self._async_query_state_async_query_state())
170  value = None
171  if self._value_template_value_template:
172  value = self._value_template_value_template.async_render_with_possible_json_value(
173  payload, None
174  )
175  self._attr_is_on_attr_is_on = None
176  if payload or value:
177  self._attr_is_on_attr_is_on = (value or payload).lower() == "true"
178  self._process_manual_data_process_manual_data(payload)
179  self.async_write_ha_stateasync_write_ha_state()
180 
181  async def async_update(self) -> None:
182  """Update the entity.
183 
184  Only used by the generic entity update service.
185  """
186  await self._update_entity_state_update_entity_state(dt_util.now())
187 
188  async def async_turn_on(self, **kwargs: Any) -> None:
189  """Turn the device on."""
190  if await self._switch_switch(self._command_on_command_on) and not self._command_state_command_state:
191  self._attr_is_on_attr_is_on = True
192  self.async_write_ha_stateasync_write_ha_state()
193  await self._update_entity_state_update_entity_state()
194 
195  async def async_turn_off(self, **kwargs: Any) -> None:
196  """Turn the device off."""
197  if await self._switch_switch(self._command_off_command_off) and not self._command_state_command_state:
198  self._attr_is_on_attr_is_on = False
199  self.async_write_ha_stateasync_write_ha_state()
200  await self._update_entity_state_update_entity_state()
None _update_entity_state(self, datetime|None now=None)
Definition: switch.py:151
str|None _async_query_state_value(self, str command)
Definition: switch.py:123
None __init__(self, ConfigType config, str object_id, str command_on, str command_off, str|None command_state, Template|None value_template, int timeout, timedelta scan_interval)
Definition: switch.py:85
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
str|UndefinedType|None name(self)
Definition: entity.py:738
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: switch.py:37
int async_call_shell_with_timeout(str command, int timeout, *bool log_return_code=True)
Definition: utils.py:14
str|None async_check_output_or_log(str command, int timeout)
Definition: utils.py:44
CALLBACK_TYPE async_track_time_interval(HomeAssistant hass, Callable[[datetime], Coroutine[Any, Any, None]|None] action, timedelta interval, *str|None name=None, bool|None cancel_on_shutdown=None)
Definition: event.py:1679