Home Assistant Unofficial Reference 2024.12.1
expose.py
Go to the documentation of this file.
1 """Exposures to KNX bus."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 import logging
7 
8 from xknx import XKNX
9 from xknx.devices import DateDevice, DateTimeDevice, ExposeSensor, TimeDevice
10 from xknx.dpt import DPTNumeric, DPTString
11 from xknx.exceptions import ConversionError
12 from xknx.remote_value import RemoteValueSensor
13 
14 from homeassistant.const import (
15  CONF_ENTITY_ID,
16  CONF_VALUE_TEMPLATE,
17  STATE_OFF,
18  STATE_ON,
19  STATE_UNAVAILABLE,
20  STATE_UNKNOWN,
21 )
22 from homeassistant.core import (
23  Event,
24  EventStateChangedData,
25  HomeAssistant,
26  State,
27  callback,
28 )
29 from homeassistant.exceptions import TemplateError
30 from homeassistant.helpers.event import async_track_state_change_event
31 from homeassistant.helpers.template import Template
32 from homeassistant.helpers.typing import ConfigType, StateType
33 
34 from .const import CONF_RESPOND_TO_READ, KNX_ADDRESS
35 from .schema import ExposeSchema
36 
37 _LOGGER = logging.getLogger(__name__)
38 
39 
40 @callback
42  hass: HomeAssistant, xknx: XKNX, config: ConfigType
43 ) -> KNXExposeSensor | KNXExposeTime:
44  """Create exposures from config."""
45 
46  expose_type = config[ExposeSchema.CONF_KNX_EXPOSE_TYPE]
47 
48  exposure: KNXExposeSensor | KNXExposeTime
49  if (
50  isinstance(expose_type, str)
51  and expose_type.lower() in ExposeSchema.EXPOSE_TIME_TYPES
52  ):
53  exposure = KNXExposeTime(
54  xknx=xknx,
55  config=config,
56  )
57  else:
58  exposure = KNXExposeSensor(
59  hass,
60  xknx=xknx,
61  config=config,
62  )
63  exposure.async_register()
64  return exposure
65 
66 
68  """Object to Expose Home Assistant entity to KNX bus."""
69 
70  def __init__(
71  self,
72  hass: HomeAssistant,
73  xknx: XKNX,
74  config: ConfigType,
75  ) -> None:
76  """Initialize of Expose class."""
77  self.hasshass = hass
78  self.xknxxknx = xknx
79 
80  self.entity_id: str = config[CONF_ENTITY_ID]
81  self.expose_attribute: str | None = config.get(
82  ExposeSchema.CONF_KNX_EXPOSE_ATTRIBUTE
83  )
84  self.expose_defaultexpose_default = config.get(ExposeSchema.CONF_KNX_EXPOSE_DEFAULT)
85  self.expose_typeexpose_type: int | str = config[ExposeSchema.CONF_KNX_EXPOSE_TYPE]
86  self.value_template: Template | None = config.get(CONF_VALUE_TEMPLATE)
87 
88  self._remove_listener_remove_listener: Callable[[], None] | None = None
89  self.device: ExposeSensor = ExposeSensor(
90  xknx=self.xknxxknx,
91  name=f"{self.entity_id}__{self.expose_attribute or "state"}",
92  group_address=config[KNX_ADDRESS],
93  respond_to_read=config[CONF_RESPOND_TO_READ],
94  value_type=self.expose_typeexpose_type,
95  cooldown=config[ExposeSchema.CONF_KNX_EXPOSE_COOLDOWN],
96  )
97 
98  @callback
99  def async_register(self) -> None:
100  """Register listener."""
102  self.hasshass, [self.entity_id], self._async_entity_changed_async_entity_changed
103  )
104  self.xknxxknx.devices.async_add(self.device)
105  self._init_expose_state_init_expose_state()
106 
107  @callback
108  def _init_expose_state(self) -> None:
109  """Initialize state of the exposure."""
110  init_state = self.hasshass.states.get(self.entity_id)
111  state_value = self._get_expose_value_get_expose_value(init_state)
112  try:
113  self.device.sensor_value.value = state_value
114  except ConversionError:
115  _LOGGER.exception("Error during sending of expose sensor value")
116 
117  @callback
118  def async_remove(self) -> None:
119  """Prepare for deletion."""
120  if self._remove_listener_remove_listener is not None:
121  self._remove_listener_remove_listener()
122  self._remove_listener_remove_listener = None
123  self.xknxxknx.devices.async_remove(self.device)
124 
125  def _get_expose_value(self, state: State | None) -> bool | int | float | str | None:
126  """Extract value from state."""
127  if state is None or state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE):
128  if self.expose_defaultexpose_default is None:
129  return None
130  value = self.expose_defaultexpose_default
131  elif self.expose_attribute is not None:
132  _attr = state.attributes.get(self.expose_attribute)
133  value = _attr if _attr is not None else self.expose_defaultexpose_default
134  else:
135  value = state.state
136 
137  if self.value_template is not None:
138  try:
139  value = self.value_template.async_render_with_possible_json_value(
140  value, error_value=None
141  )
142  except (TemplateError, TypeError, ValueError) as err:
143  _LOGGER.warning(
144  "Error rendering value template for KNX expose %s %s: %s",
145  self.device.name,
146  self.value_template.template,
147  err,
148  )
149  return None
150 
151  if self.expose_typeexpose_type == "binary":
152  if value in (1, STATE_ON, "True"):
153  return True
154  if value in (0, STATE_OFF, "False"):
155  return False
156  if value is not None and (
157  isinstance(self.device.sensor_value, RemoteValueSensor)
158  ):
159  try:
160  if issubclass(self.device.sensor_value.dpt_class, DPTNumeric):
161  return float(value)
162  if issubclass(self.device.sensor_value.dpt_class, DPTString):
163  # DPT 16.000 only allows up to 14 Bytes
164  return str(value)[:14]
165  except (ValueError, TypeError) as err:
166  _LOGGER.warning(
167  'Could not expose %s %s value "%s" to KNX: Conversion failed: %s',
168  self.entity_id,
169  self.expose_attribute or "state",
170  value,
171  err,
172  )
173  return None
174  return value # type: ignore[no-any-return]
175 
176  async def _async_entity_changed(self, event: Event[EventStateChangedData]) -> None:
177  """Handle entity change."""
178  new_state = event.data["new_state"]
179  if (new_value := self._get_expose_value_get_expose_value(new_state)) is None:
180  return
181  old_state = event.data["old_state"]
182  # don't use default value for comparison on first state change (old_state is None)
183  old_value = self._get_expose_value_get_expose_value(old_state) if old_state is not None else None
184  # don't send same value sequentially
185  if new_value != old_value:
186  await self._async_set_knx_value_async_set_knx_value(new_value)
187 
188  async def _async_set_knx_value(self, value: StateType) -> None:
189  """Set new value on xknx ExposeSensor."""
190  try:
191  await self.device.set(value)
192  except ConversionError as err:
193  _LOGGER.warning(
194  'Could not expose %s %s value "%s" to KNX: %s',
195  self.entity_id,
196  self.expose_attribute or "state",
197  value,
198  err,
199  )
200 
201 
203  """Object to Expose Time/Date object to KNX bus."""
204 
205  def __init__(self, xknx: XKNX, config: ConfigType) -> None:
206  """Initialize of Expose class."""
207  self.xknxxknx = xknx
208  expose_type = config[ExposeSchema.CONF_KNX_EXPOSE_TYPE]
209  xknx_device_cls: type[DateDevice | DateTimeDevice | TimeDevice]
210  match expose_type:
211  case ExposeSchema.CONF_DATE:
212  xknx_device_cls = DateDevice
213  case ExposeSchema.CONF_DATETIME:
214  xknx_device_cls = DateTimeDevice
215  case ExposeSchema.CONF_TIME:
216  xknx_device_cls = TimeDevice
217  self.devicedevice = xknx_device_cls(
218  self.xknxxknx,
219  name=expose_type.capitalize(),
220  localtime=True,
221  group_address=config[KNX_ADDRESS],
222  )
223 
224  @callback
225  def async_register(self) -> None:
226  """Register listener."""
227  self.xknxxknx.devices.async_add(self.devicedevice)
228 
229  @callback
230  def async_remove(self) -> None:
231  """Prepare for deletion."""
232  self.xknxxknx.devices.async_remove(self.devicedevice)
bool|int|float|str|None _get_expose_value(self, State|None state)
Definition: expose.py:125
None _async_entity_changed(self, Event[EventStateChangedData] event)
Definition: expose.py:176
None __init__(self, HomeAssistant hass, XKNX xknx, ConfigType config)
Definition: expose.py:75
None _async_set_knx_value(self, StateType value)
Definition: expose.py:188
None __init__(self, XKNX xknx, ConfigType config)
Definition: expose.py:205
KNXExposeSensor|KNXExposeTime create_knx_exposure(HomeAssistant hass, XKNX xknx, ConfigType config)
Definition: expose.py:43
CALLBACK_TYPE async_track_state_change_event(HomeAssistant hass, str|Iterable[str] entity_ids, Callable[[Event[EventStateChangedData]], Any] action, HassJobType|None job_type=None)
Definition: event.py:314