Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Support for MQTT room presence detection."""
2 
3 from __future__ import annotations
4 
5 from datetime import timedelta
6 from functools import lru_cache
7 import logging
8 from typing import Any
9 
10 import voluptuous as vol
11 
12 from homeassistant.components import mqtt
13 from homeassistant.components.mqtt import CONF_STATE_TOPIC
15  PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
16  SensorEntity,
17 )
18 from homeassistant.const import (
19  ATTR_DEVICE_ID,
20  ATTR_ID,
21  CONF_DEVICE_ID,
22  CONF_NAME,
23  CONF_TIMEOUT,
24  CONF_UNIQUE_ID,
25  STATE_NOT_HOME,
26 )
27 from homeassistant.core import HomeAssistant, callback
29 from homeassistant.helpers.entity_platform import AddEntitiesCallback
30 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
31 from homeassistant.util import dt as dt_util, slugify
32 from homeassistant.util.json import json_loads
33 
34 _LOGGER = logging.getLogger(__name__)
35 
36 ATTR_DISTANCE = "distance"
37 ATTR_ROOM = "room"
38 
39 CONF_AWAY_TIMEOUT = "away_timeout"
40 
41 DEFAULT_AWAY_TIMEOUT = 0
42 DEFAULT_NAME = "Room Sensor"
43 DEFAULT_TIMEOUT = 5
44 DEFAULT_TOPIC = "room_presence"
45 
46 PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
47  {
48  vol.Required(CONF_DEVICE_ID): cv.string,
49  vol.Required(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
50  vol.Optional(CONF_AWAY_TIMEOUT, default=DEFAULT_AWAY_TIMEOUT): cv.positive_int,
51  vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
52  vol.Optional(CONF_UNIQUE_ID): cv.string,
53  }
54 ).extend(mqtt.MQTT_RO_SCHEMA.schema)
55 
56 
57 @lru_cache(maxsize=256)
58 def _slugify_upper(string: str) -> str:
59  """Return a slugified version of string, uppercased."""
60  return slugify(string).upper()
61 
62 
63 MQTT_PAYLOAD = vol.Schema(
64  vol.All(
65  json_loads,
66  vol.Schema(
67  {
68  vol.Required(ATTR_ID): cv.string,
69  vol.Required(ATTR_DISTANCE): vol.Coerce(float),
70  },
71  extra=vol.ALLOW_EXTRA,
72  ),
73  )
74 )
75 
76 
78  hass: HomeAssistant,
79  config: ConfigType,
80  async_add_entities: AddEntitiesCallback,
81  discovery_info: DiscoveryInfoType | None = None,
82 ) -> None:
83  """Set up MQTT room Sensor."""
84  # Make sure MQTT integration is enabled and the client is available
85  # We cannot count on dependencies as the sensor platform setup
86  # also will be triggered when mqtt is loading the `sensor` platform
87  if not await mqtt.async_wait_for_mqtt_client(hass):
88  _LOGGER.error("MQTT integration is not available")
89  return
91  [
93  config.get(CONF_NAME),
94  config[CONF_STATE_TOPIC],
95  config[CONF_DEVICE_ID],
96  config[CONF_TIMEOUT],
97  config[CONF_AWAY_TIMEOUT],
98  config.get(CONF_UNIQUE_ID),
99  )
100  ]
101  )
102 
103 
105  """Representation of a room sensor that is updated via MQTT."""
106 
107  def __init__(
108  self,
109  name: str | None,
110  state_topic: str,
111  device_id: str,
112  timeout: int,
113  consider_home: int,
114  unique_id: str | None,
115  ) -> None:
116  """Initialize the sensor."""
117  self._attr_unique_id_attr_unique_id = unique_id
118 
119  self._state_state = STATE_NOT_HOME
120  self._name_name = name
121  self._state_topic_state_topic = f"{state_topic}/+"
122  self._device_id_device_id = _slugify_upper(device_id)
123  self._timeout_timeout = timeout
124  self._consider_home_consider_home = (
125  timedelta(seconds=consider_home) if consider_home else None
126  )
127  self._distance_distance = None
128  self._updated_updated = None
129 
130  async def async_added_to_hass(self) -> None:
131  """Subscribe to MQTT events."""
132 
133  @callback
134  def update_state(device_id, room, distance):
135  """Update the sensor state."""
136  self._state_state = room
137  self._distance_distance = distance
138  self._updated_updated = dt_util.utcnow()
139 
140  self.async_write_ha_stateasync_write_ha_state()
141 
142  @callback
143  def message_received(msg):
144  """Handle new MQTT messages."""
145  try:
146  data = MQTT_PAYLOAD(msg.payload)
147  except vol.MultipleInvalid as error:
148  _LOGGER.debug("Skipping update because of malformatted data: %s", error)
149  return
150 
151  device = _parse_update_data(msg.topic, data)
152  if device.get(CONF_DEVICE_ID) == self._device_id_device_id:
153  if self._distance_distance is None or self._updated_updated is None:
154  update_state(**device)
155  else:
156  # update if:
157  # device is in the same room OR
158  # device is closer to another room OR
159  # last update from other room was too long ago
160  timediff = dt_util.utcnow() - self._updated_updated
161  if (
162  device.get(ATTR_ROOM) == self._state_state
163  or device.get(ATTR_DISTANCE) < self._distance_distance
164  or timediff.total_seconds() >= self._timeout_timeout
165  ):
166  update_state(**device)
167 
168  await mqtt.async_subscribe(self.hasshass, self._state_topic_state_topic, message_received, 1)
169 
170  @property
171  def name(self):
172  """Return the name of the sensor."""
173  return self._name_name
174 
175  @property
177  """Return the state attributes."""
178  return {ATTR_DISTANCE: self._distance_distance}
179 
180  @property
181  def native_value(self):
182  """Return the current room of the entity."""
183  return self._state_state
184 
185  def update(self) -> None:
186  """Update the state for absent devices."""
187  if (
188  self._updated_updated
189  and self._consider_home_consider_home
190  and dt_util.utcnow() - self._updated_updated > self._consider_home_consider_home
191  ):
192  self._state_state = STATE_NOT_HOME
193 
194 
195 def _parse_update_data(topic: str, data: dict[str, Any]) -> dict[str, Any]:
196  """Parse the room presence update."""
197  parts = topic.split("/")
198  room = parts[-1]
199  device_id = _slugify_upper(data.get(ATTR_ID))
200  distance = data.get("distance")
201  return {ATTR_DEVICE_ID: device_id, ATTR_ROOM: room, ATTR_DISTANCE: distance}
None __init__(self, str|None name, str state_topic, str device_id, int timeout, int consider_home, str|None unique_id)
Definition: sensor.py:115
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: sensor.py:82
dict[str, Any] _parse_update_data(str topic, dict[str, Any] data)
Definition: sensor.py:195