Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Allows to configure custom shell commands to turn a value for a sensor."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Mapping
7 from datetime import datetime, timedelta
8 import json
9 from typing import Any, cast
10 
11 from jsonpath import jsonpath
12 
13 from homeassistant.components.sensor import SensorDeviceClass
14 from homeassistant.components.sensor.helpers import async_parse_date_datetime
15 from homeassistant.const import (
16  CONF_COMMAND,
17  CONF_NAME,
18  CONF_SCAN_INTERVAL,
19  CONF_VALUE_TEMPLATE,
20 )
21 from homeassistant.core import HomeAssistant
22 from homeassistant.exceptions import TemplateError
23 from homeassistant.helpers.entity_platform import AddEntitiesCallback
24 from homeassistant.helpers.event import async_track_time_interval
25 from homeassistant.helpers.template import Template
26 from homeassistant.helpers.trigger_template_entity import ManualTriggerSensorEntity
27 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
28 from homeassistant.util import dt as dt_util
29 
30 from .const import (
31  CONF_COMMAND_TIMEOUT,
32  CONF_JSON_ATTRIBUTES,
33  CONF_JSON_ATTRIBUTES_PATH,
34  LOGGER,
35  TRIGGER_ENTITY_OPTIONS,
36 )
37 from .utils import async_check_output_or_log
38 
39 DEFAULT_NAME = "Command Sensor"
40 
41 SCAN_INTERVAL = timedelta(seconds=60)
42 
43 
45  hass: HomeAssistant,
46  config: ConfigType,
47  async_add_entities: AddEntitiesCallback,
48  discovery_info: DiscoveryInfoType | None = None,
49 ) -> None:
50  """Set up the Command Sensor."""
51  if not discovery_info:
52  return
53 
54  discovery_info = cast(DiscoveryInfoType, discovery_info)
55  sensor_config = discovery_info
56 
57  command: str = sensor_config[CONF_COMMAND]
58  command_timeout: int = sensor_config[CONF_COMMAND_TIMEOUT]
59  json_attributes: list[str] | None = sensor_config.get(CONF_JSON_ATTRIBUTES)
60  json_attributes_path: str | None = sensor_config.get(CONF_JSON_ATTRIBUTES_PATH)
61  scan_interval: timedelta = sensor_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
62  value_template: Template | None = sensor_config.get(CONF_VALUE_TEMPLATE)
63  data = CommandSensorData(hass, command, command_timeout)
64 
65  trigger_entity_config = {
66  CONF_NAME: Template(sensor_config[CONF_NAME], hass),
67  **{k: v for k, v in sensor_config.items() if k in TRIGGER_ENTITY_OPTIONS},
68  }
69 
71  [
73  data,
74  trigger_entity_config,
75  value_template,
76  json_attributes,
77  json_attributes_path,
78  scan_interval,
79  )
80  ]
81  )
82 
83 
85  """Representation of a sensor that is using shell commands."""
86 
87  _attr_should_poll = False
88 
89  def __init__(
90  self,
91  data: CommandSensorData,
92  config: ConfigType,
93  value_template: Template | None,
94  json_attributes: list[str] | None,
95  json_attributes_path: str | None,
96  scan_interval: timedelta,
97  ) -> None:
98  """Initialize the sensor."""
99  super().__init__(self.hasshasshass, config)
100  self.datadata = data
101  self._attr_extra_state_attributes_attr_extra_state_attributes: dict[str, Any] = {}
102  self._json_attributes_json_attributes = json_attributes
103  self._json_attributes_path_json_attributes_path = json_attributes_path
104  self._attr_native_value_attr_native_value = None
105  self._value_template_value_template = value_template
106  self._scan_interval_scan_interval = scan_interval
107  self._process_updates_process_updates: asyncio.Lock | None = None
108 
109  @property
110  def extra_state_attributes(self) -> dict[str, Any]:
111  """Return extra state attributes."""
112  return self._attr_extra_state_attributes_attr_extra_state_attributes
113 
114  async def async_added_to_hass(self) -> None:
115  """Call when entity about to be added to hass."""
116  await super().async_added_to_hass()
117  await self._update_entity_state_update_entity_state()
118  self.async_on_removeasync_on_remove(
120  self.hasshasshass,
121  self._update_entity_state_update_entity_state,
122  self._scan_interval_scan_interval,
123  name=f"Command Line Sensor - {self.name}",
124  cancel_on_shutdown=True,
125  ),
126  )
127 
128  async def _update_entity_state(self, now: datetime | None = None) -> None:
129  """Update the state of the entity."""
130  if self._process_updates_process_updates is None:
131  self._process_updates_process_updates = asyncio.Lock()
132 
133  if self._process_updates_process_updates.locked():
134  LOGGER.warning(
135  "Updating Command Line Sensor %s took longer than the scheduled update interval %s",
136  self.namenamename,
137  self._scan_interval_scan_interval,
138  )
139  return
140 
141  async with self._process_updates_process_updates:
142  await self._async_update_async_update()
143 
144  async def _async_update(self) -> None:
145  """Get the latest data and updates the state."""
146  await self.datadata.async_update()
147  value = self.datadata.value
148 
149  if self._json_attributes_json_attributes:
150  self._attr_extra_state_attributes_attr_extra_state_attributes = {}
151  if value:
152  try:
153  json_dict = json.loads(value)
154  if self._json_attributes_path_json_attributes_path is not None:
155  json_dict = jsonpath(json_dict, self._json_attributes_path_json_attributes_path)
156  # jsonpath will always store the result in json_dict[0]
157  # so the next line happens to work exactly as needed to
158  # find the result
159  if isinstance(json_dict, list):
160  json_dict = json_dict[0]
161  if isinstance(json_dict, Mapping):
162  self._attr_extra_state_attributes_attr_extra_state_attributes = {
163  k: json_dict[k]
164  for k in self._json_attributes_json_attributes
165  if k in json_dict
166  }
167  else:
168  LOGGER.warning("JSON result was not a dictionary")
169  except ValueError:
170  LOGGER.warning("Unable to parse output as JSON: %s", value)
171  else:
172  LOGGER.warning("Empty reply found when expecting JSON data")
173  if self._value_template_value_template is None:
174  self._attr_native_value_attr_native_value = None
175  self._process_manual_data_process_manual_data(value)
176  return
177 
178  self._attr_native_value_attr_native_value = None
179  if self._value_template_value_template is not None and value is not None:
180  value = self._value_template_value_template.async_render_with_possible_json_value(
181  value,
182  None,
183  )
184 
185  if self.device_classdevice_classdevice_class not in {
186  SensorDeviceClass.DATE,
187  SensorDeviceClass.TIMESTAMP,
188  }:
189  self._attr_native_value_attr_native_value = value
190  elif value is not None:
191  self._attr_native_value_attr_native_value = async_parse_date_datetime(
192  value, self.entity_identity_id, self.device_classdevice_classdevice_class
193  )
194 
195  self._process_manual_data_process_manual_data(value)
196  self.async_write_ha_stateasync_write_ha_state()
197 
198  async def async_update(self) -> None:
199  """Update the entity.
200 
201  Only used by the generic entity update service.
202  """
203  await self._update_entity_state_update_entity_state(dt_util.now())
204 
205 
207  """The class for handling the data retrieval."""
208 
209  def __init__(self, hass: HomeAssistant, command: str, command_timeout: int) -> None:
210  """Initialize the data object."""
211  self.valuevalue: str | None = None
212  self.hasshass = hass
213  self.commandcommand = command
214  self.timeouttimeout = command_timeout
215 
216  async def async_update(self) -> None:
217  """Get the latest data with a shell command."""
218  command = self.commandcommand
219 
220  if " " not in command:
221  prog = command
222  args = None
223  args_compiled = None
224  else:
225  prog, args = command.split(" ", 1)
226  args_compiled = Template(args, self.hasshass)
227 
228  if args_compiled:
229  try:
230  args_to_render = {"arguments": args}
231  rendered_args = args_compiled.async_render(args_to_render)
232  except TemplateError as ex:
233  LOGGER.exception("Error rendering command template: %s", ex)
234  return
235  else:
236  rendered_args = None
237 
238  if rendered_args == args:
239  # No template used. default behavior
240  pass
241  else:
242  # Template used. Construct the string used in the shell
243  command = f"{prog} {rendered_args}"
244 
245  LOGGER.debug("Running command: %s", command)
246  self.valuevalue = await async_check_output_or_log(command, self.timeouttimeout)
None __init__(self, HomeAssistant hass, str command, int command_timeout)
Definition: sensor.py:209
None __init__(self, CommandSensorData data, ConfigType config, Template|None value_template, list[str]|None json_attributes, str|None json_attributes_path, timedelta scan_interval)
Definition: sensor.py:97
None _update_entity_state(self, datetime|None now=None)
Definition: sensor.py:128
SensorDeviceClass|None device_class(self)
Definition: __init__.py:313
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: sensor.py:49
str|None async_check_output_or_log(str command, int timeout)
Definition: utils.py:44
datetime|date|None async_parse_date_datetime(str value, str entity_id, SensorDeviceClass|str|None device_class)
Definition: helpers.py:19
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