Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Support for getting data from websites with scraping."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any, cast
7 
8 import voluptuous as vol
9 
10 from homeassistant.components.sensor import CONF_STATE_CLASS, SensorDeviceClass
11 from homeassistant.components.sensor.helpers import async_parse_date_datetime
12 from homeassistant.const import (
13  CONF_ATTRIBUTE,
14  CONF_DEVICE_CLASS,
15  CONF_ICON,
16  CONF_NAME,
17  CONF_UNIQUE_ID,
18  CONF_UNIT_OF_MEASUREMENT,
19  CONF_VALUE_TEMPLATE,
20 )
21 from homeassistant.core import HomeAssistant, callback
22 from homeassistant.exceptions import PlatformNotReady
23 from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
24 from homeassistant.helpers.entity_platform import AddEntitiesCallback
25 from homeassistant.helpers.template import Template
27  CONF_AVAILABILITY,
28  CONF_PICTURE,
29  TEMPLATE_SENSOR_BASE_SCHEMA,
30  ManualTriggerEntity,
31  ManualTriggerSensorEntity,
32 )
33 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
34 from homeassistant.helpers.update_coordinator import CoordinatorEntity
35 
36 from . import ScrapeConfigEntry
37 from .const import CONF_INDEX, CONF_SELECT, DOMAIN
38 from .coordinator import ScrapeCoordinator
39 
40 _LOGGER = logging.getLogger(__name__)
41 
42 TRIGGER_ENTITY_OPTIONS = (
43  CONF_AVAILABILITY,
44  CONF_DEVICE_CLASS,
45  CONF_ICON,
46  CONF_PICTURE,
47  CONF_UNIQUE_ID,
48  CONF_STATE_CLASS,
49  CONF_UNIT_OF_MEASUREMENT,
50 )
51 
52 
54  hass: HomeAssistant,
55  config: ConfigType,
56  async_add_entities: AddEntitiesCallback,
57  discovery_info: DiscoveryInfoType | None = None,
58 ) -> None:
59  """Set up the Web scrape sensor."""
60  discovery_info = cast(DiscoveryInfoType, discovery_info)
61  coordinator: ScrapeCoordinator = discovery_info["coordinator"]
62  sensors_config: list[ConfigType] = discovery_info["configs"]
63 
64  await coordinator.async_refresh()
65  if coordinator.data is None:
66  raise PlatformNotReady
67 
68  entities: list[ScrapeSensor] = []
69  for sensor_config in sensors_config:
70  trigger_entity_config = {CONF_NAME: sensor_config[CONF_NAME]}
71  for key in TRIGGER_ENTITY_OPTIONS:
72  if key not in sensor_config:
73  continue
74  trigger_entity_config[key] = sensor_config[key]
75 
76  entities.append(
78  hass,
79  coordinator,
80  trigger_entity_config,
81  sensor_config[CONF_SELECT],
82  sensor_config.get(CONF_ATTRIBUTE),
83  sensor_config[CONF_INDEX],
84  sensor_config.get(CONF_VALUE_TEMPLATE),
85  True,
86  )
87  )
88 
89  async_add_entities(entities)
90 
91 
93  hass: HomeAssistant,
94  entry: ScrapeConfigEntry,
95  async_add_entities: AddEntitiesCallback,
96 ) -> None:
97  """Set up the Scrape sensor entry."""
98  entities: list = []
99 
100  coordinator = entry.runtime_data
101  config = dict(entry.options)
102  for sensor in config["sensor"]:
103  sensor_config: ConfigType = vol.Schema(
104  TEMPLATE_SENSOR_BASE_SCHEMA.schema, extra=vol.ALLOW_EXTRA
105  )(sensor)
106 
107  name: str = sensor_config[CONF_NAME]
108  value_string: str | None = sensor_config.get(CONF_VALUE_TEMPLATE)
109 
110  value_template: Template | None = (
111  Template(value_string, hass) if value_string is not None else None
112  )
113 
114  trigger_entity_config: dict[str, str | Template | None] = {CONF_NAME: name}
115  for key in TRIGGER_ENTITY_OPTIONS:
116  if key not in sensor_config:
117  continue
118  if key == CONF_AVAILABILITY:
119  trigger_entity_config[key] = Template(sensor_config[key], hass)
120  continue
121  trigger_entity_config[key] = sensor_config[key]
122 
123  entities.append(
124  ScrapeSensor(
125  hass,
126  coordinator,
127  trigger_entity_config,
128  sensor_config[CONF_SELECT],
129  sensor_config.get(CONF_ATTRIBUTE),
130  sensor_config[CONF_INDEX],
131  value_template,
132  False,
133  )
134  )
135 
136  async_add_entities(entities)
137 
138 
139 class ScrapeSensor(CoordinatorEntity[ScrapeCoordinator], ManualTriggerSensorEntity):
140  """Representation of a web scrape sensor."""
141 
142  def __init__(
143  self,
144  hass: HomeAssistant,
145  coordinator: ScrapeCoordinator,
146  trigger_entity_config: ConfigType,
147  select: str,
148  attr: str | None,
149  index: int,
150  value_template: Template | None,
151  yaml: bool,
152  ) -> None:
153  """Initialize a web scrape sensor."""
154  CoordinatorEntity.__init__(self, coordinator)
155  ManualTriggerSensorEntity.__init__(self, hass, trigger_entity_config)
156  self._select_select = select
157  self._attr_attr = attr
158  self._index_index = index
159  self._value_template_value_template = value_template
160  self._attr_native_value_attr_native_value = None
161  if not yaml and (unique_id := trigger_entity_config.get(CONF_UNIQUE_ID)):
162  self._attr_name_attr_name = None
163  self._attr_has_entity_name_attr_has_entity_name = True
164  self._attr_device_info_attr_device_info = DeviceInfo(
165  entry_type=DeviceEntryType.SERVICE,
166  identifiers={(DOMAIN, unique_id)},
167  manufacturer="Scrape",
168  name=self.namenamenamename,
169  )
170 
171  def _extract_value(self) -> Any:
172  """Parse the html extraction in the executor."""
173  raw_data = self.coordinator.data
174  value: str | list[str] | None
175  try:
176  if self._attr_attr is not None:
177  value = raw_data.select(self._select_select)[self._index_index][self._attr_attr]
178  else:
179  tag = raw_data.select(self._select_select)[self._index_index]
180  if tag.name in ("style", "script", "template"):
181  value = tag.string
182  else:
183  value = tag.text
184  except IndexError:
185  _LOGGER.warning("Index '%s' not found in %s", self._index_index, self.entity_identity_id)
186  value = None
187  except KeyError:
188  _LOGGER.warning(
189  "Attribute '%s' not found in %s", self._attr_attr, self.entity_identity_id
190  )
191  value = None
192  _LOGGER.debug("Parsed value: %s", value)
193  return value
194 
195  async def async_added_to_hass(self) -> None:
196  """Ensure the data from the initial update is reflected in the state."""
197  await super().async_added_to_hass()
198  self._async_update_from_rest_data_async_update_from_rest_data()
199 
200  def _async_update_from_rest_data(self) -> None:
201  """Update state from the rest data."""
202  value = self._extract_value_extract_value()
203  raw_value = value
204 
205  if (template := self._value_template_value_template) is not None:
206  value = template.async_render_with_possible_json_value(value, None)
207 
208  if self.device_classdevice_classdevice_class not in {
209  SensorDeviceClass.DATE,
210  SensorDeviceClass.TIMESTAMP,
211  }:
212  self._attr_native_value_attr_native_value = value
213  self._process_manual_data_process_manual_data(raw_value)
214  return
215 
216  self._attr_native_value_attr_native_value = async_parse_date_datetime(
217  value, self.entity_identity_id, self.device_classdevice_classdevice_class
218  )
219  self._process_manual_data_process_manual_data(raw_value)
220  self.async_write_ha_stateasync_write_ha_state()
221 
222  @property
223  def available(self) -> bool:
224  """Return if entity is available."""
225  available1 = CoordinatorEntity.available.fget(self) # type: ignore[attr-defined]
226  available2 = ManualTriggerEntity.available.fget(self) # type: ignore[attr-defined]
227  return bool(available1 and available2)
228 
229  @callback
230  def _handle_coordinator_update(self) -> None:
231  """Handle updated data from the coordinator."""
232  self._async_update_from_rest_data_async_update_from_rest_data()
None __init__(self, HomeAssistant hass, ScrapeCoordinator coordinator, ConfigType trigger_entity_config, str select, str|None attr, int index, Template|None value_template, bool yaml)
Definition: sensor.py:152
SensorDeviceClass|None device_class(self)
Definition: __init__.py:313
str|UndefinedType|None name(self)
Definition: entity.py:738
None async_setup_entry(HomeAssistant hass, ScrapeConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:96
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: sensor.py:58
datetime|date|None async_parse_date_datetime(str value, str entity_id, SensorDeviceClass|str|None device_class)
Definition: helpers.py:19