Home Assistant Unofficial Reference 2024.12.1
device_tracker.py
Go to the documentation of this file.
1 """Support for tracking MQTT enabled devices identified."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 import logging
7 from typing import TYPE_CHECKING
8 
9 import voluptuous as vol
10 
11 from homeassistant.components import device_tracker
12 from homeassistant.components.device_tracker import SourceType, TrackerEntity
13 from homeassistant.config_entries import ConfigEntry
14 from homeassistant.const import (
15  ATTR_GPS_ACCURACY,
16  ATTR_LATITUDE,
17  ATTR_LONGITUDE,
18  CONF_NAME,
19  CONF_VALUE_TEMPLATE,
20  STATE_HOME,
21  STATE_NOT_HOME,
22 )
23 from homeassistant.core import HomeAssistant, callback
25 from homeassistant.helpers.entity_platform import AddEntitiesCallback
26 from homeassistant.helpers.service_info.mqtt import ReceivePayloadType
27 from homeassistant.helpers.typing import ConfigType, VolSchemaType
28 
29 from . import subscription
30 from .config import MQTT_BASE_SCHEMA
31 from .const import CONF_PAYLOAD_RESET, CONF_STATE_TOPIC
32 from .entity import CONF_JSON_ATTRS_TOPIC, MqttEntity, async_setup_entity_entry_helper
33 from .models import MqttValueTemplate, ReceiveMessage
34 from .schemas import MQTT_ENTITY_COMMON_SCHEMA
35 from .util import valid_subscribe_topic
36 
37 _LOGGER = logging.getLogger(__name__)
38 
39 PARALLEL_UPDATES = 0
40 
41 CONF_PAYLOAD_HOME = "payload_home"
42 CONF_PAYLOAD_NOT_HOME = "payload_not_home"
43 CONF_SOURCE_TYPE = "source_type"
44 
45 DEFAULT_PAYLOAD_RESET = "None"
46 DEFAULT_SOURCE_TYPE = SourceType.GPS
47 
48 
49 def valid_config(config: ConfigType) -> ConfigType:
50  """Check if there is a state topic or json_attributes_topic."""
51  if CONF_STATE_TOPIC not in config and CONF_JSON_ATTRS_TOPIC not in config:
52  raise vol.Invalid(
53  f"Invalid device tracker config, missing {CONF_STATE_TOPIC} or {CONF_JSON_ATTRS_TOPIC}, got: {config}"
54  )
55  return config
56 
57 
58 PLATFORM_SCHEMA_MODERN_BASE = MQTT_BASE_SCHEMA.extend(
59  {
60  vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic,
61  vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
62  vol.Optional(CONF_NAME): vol.Any(cv.string, None),
63  vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string,
64  vol.Optional(CONF_PAYLOAD_NOT_HOME, default=STATE_NOT_HOME): cv.string,
65  vol.Optional(CONF_PAYLOAD_RESET, default=DEFAULT_PAYLOAD_RESET): cv.string,
66  vol.Optional(CONF_SOURCE_TYPE, default=DEFAULT_SOURCE_TYPE): vol.Coerce(
67  SourceType
68  ),
69  },
70 ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
71 PLATFORM_SCHEMA_MODERN = vol.All(PLATFORM_SCHEMA_MODERN_BASE, valid_config)
72 
73 
74 DISCOVERY_SCHEMA = vol.All(
75  PLATFORM_SCHEMA_MODERN_BASE.extend({}, extra=vol.REMOVE_EXTRA), valid_config
76 )
77 
78 
80  hass: HomeAssistant,
81  config_entry: ConfigEntry,
82  async_add_entities: AddEntitiesCallback,
83 ) -> None:
84  """Set up MQTT event through YAML and through MQTT discovery."""
86  hass,
87  config_entry,
88  MqttDeviceTracker,
89  device_tracker.DOMAIN,
90  async_add_entities,
91  DISCOVERY_SCHEMA,
92  PLATFORM_SCHEMA_MODERN,
93  )
94 
95 
96 class MqttDeviceTracker(MqttEntity, TrackerEntity):
97  """Representation of a device tracker using MQTT."""
98 
99  _default_name = None
100  _entity_id_format = device_tracker.ENTITY_ID_FORMAT
101  _location_name: str | None = None
102  _value_template: Callable[[ReceivePayloadType], ReceivePayloadType]
103 
104  @staticmethod
105  def config_schema() -> VolSchemaType:
106  """Return the config schema."""
107  return DISCOVERY_SCHEMA
108 
109  def _setup_from_config(self, config: ConfigType) -> None:
110  """(Re)Setup the entity."""
111  self._value_template_value_template = MqttValueTemplate(
112  config.get(CONF_VALUE_TEMPLATE), entity=self
113  ).async_render_with_possible_json_value
114 
115  @callback
116  def _tracker_message_received(self, msg: ReceiveMessage) -> None:
117  """Handle new MQTT messages."""
118  payload = self._value_template_value_template(msg.payload)
119  if not payload.strip(): # No output from template, ignore
120  _LOGGER.debug(
121  "Ignoring empty payload '%s' after rendering for topic %s",
122  payload,
123  msg.topic,
124  )
125  return
126  if payload == self._config_config[CONF_PAYLOAD_HOME]:
127  self._location_name_location_name = STATE_HOME
128  elif payload == self._config_config[CONF_PAYLOAD_NOT_HOME]:
129  self._location_name_location_name = STATE_NOT_HOME
130  elif payload == self._config_config[CONF_PAYLOAD_RESET]:
131  self._location_name_location_name = None
132  else:
133  if TYPE_CHECKING:
134  assert isinstance(msg.payload, str)
135  self._location_name_location_name = msg.payload
136 
137  @callback
138  def _prepare_subscribe_topics(self) -> None:
139  """(Re)Subscribe to topics."""
140  self.add_subscriptionadd_subscription(
141  CONF_STATE_TOPIC, self._tracker_message_received_tracker_message_received, {"_location_name"}
142  )
143 
144  @property
145  def force_update(self) -> bool:
146  """Do not force updates if the state is the same."""
147  return False
148 
149  async def _subscribe_topics(self) -> None:
150  """(Re)Subscribe to topics."""
151  subscription.async_subscribe_topics_internal(self.hasshasshass, self._sub_state_sub_state)
152 
153  @property
154  def latitude(self) -> float | None:
155  """Return latitude if provided in extra_state_attributes or None."""
156  if (
157  self.extra_state_attributesextra_state_attributes is not None
158  and ATTR_LATITUDE in self.extra_state_attributesextra_state_attributes
159  ):
160  latitude: float = self.extra_state_attributesextra_state_attributes[ATTR_LATITUDE]
161  return latitude
162  return None
163 
164  @property
165  def location_accuracy(self) -> int:
166  """Return location accuracy if provided in extra_state_attributes or None."""
167  if (
168  self.extra_state_attributesextra_state_attributes is not None
169  and ATTR_GPS_ACCURACY in self.extra_state_attributesextra_state_attributes
170  ):
171  accuracy: int = self.extra_state_attributesextra_state_attributes[ATTR_GPS_ACCURACY]
172  return accuracy
173  return 0
174 
175  @property
176  def longitude(self) -> float | None:
177  """Return longitude if provided in extra_state_attributes or None."""
178  if (
179  self.extra_state_attributesextra_state_attributes is not None
180  and ATTR_LONGITUDE in self.extra_state_attributesextra_state_attributes
181  ):
182  longitude: float = self.extra_state_attributesextra_state_attributes[ATTR_LONGITUDE]
183  return longitude
184  return None
185 
186  @property
187  def location_name(self) -> str | None:
188  """Return a location name for the current location of the device."""
189  return self._location_name_location_name
190 
191  @property
192  def source_type(self) -> SourceType:
193  """Return the source type, eg gps or router, of the device."""
194  source_type: SourceType = self._config_config[CONF_SOURCE_TYPE]
195  return source_type
bool add_subscription(self, str state_topic_config_key, Callable[[ReceiveMessage], None] msg_callback, set[str]|None tracked_attributes, bool disable_encoding=False)
Definition: entity.py:1484
Mapping[str, Any]|None extra_state_attributes(self)
Definition: entity.py:787
ConfigType valid_config(ConfigType config)
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
None async_setup_entity_entry_helper(HomeAssistant hass, ConfigEntry entry, type[MqttEntity]|None entity_class, str domain, AddEntitiesCallback async_add_entities, VolSchemaType discovery_schema, VolSchemaType platform_schema_modern, dict[str, type[MqttEntity]]|None schema_class_mapping=None)
Definition: entity.py:245