Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Support for package tracking sensors from 17track.net."""
2 
3 from __future__ import annotations
4 
5 from typing import Any
6 
7 from homeassistant.components import persistent_notification
8 from homeassistant.components.sensor import SensorEntity
9 from homeassistant.config_entries import ConfigEntry
10 from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_LOCATION
11 from homeassistant.core import HomeAssistant, callback
12 from homeassistant.helpers import entity_registry as er, issue_registry as ir
13 from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
14 from homeassistant.helpers.entity_platform import AddEntitiesCallback
15 from homeassistant.helpers.typing import StateType
16 from homeassistant.helpers.update_coordinator import CoordinatorEntity
17 
18 from . import SeventeenTrackCoordinator
19 from .const import (
20  ATTR_DESTINATION_COUNTRY,
21  ATTR_INFO_TEXT,
22  ATTR_ORIGIN_COUNTRY,
23  ATTR_PACKAGE_TYPE,
24  ATTR_PACKAGES,
25  ATTR_STATUS,
26  ATTR_TIMESTAMP,
27  ATTR_TRACKING_INFO_LANGUAGE,
28  ATTR_TRACKING_NUMBER,
29  ATTRIBUTION,
30  DEPRECATED_KEY,
31  DOMAIN,
32  LOGGER,
33  NOTIFICATION_DELIVERED_MESSAGE,
34  NOTIFICATION_DELIVERED_TITLE,
35  UNIQUE_ID_TEMPLATE,
36  VALUE_DELIVERED,
37 )
38 
39 
41  hass: HomeAssistant,
42  config_entry: ConfigEntry,
43  async_add_entities: AddEntitiesCallback,
44 ) -> None:
45  """Set up a 17Track sensor entry."""
46 
47  coordinator: SeventeenTrackCoordinator = hass.data[DOMAIN][config_entry.entry_id]
48  previous_tracking_numbers: set[str] = set()
49 
50  # This has been deprecated in 2024.8, will be removed in 2025.2
51  @callback
52  def _async_create_remove_entities():
53  if config_entry.data.get(DEPRECATED_KEY):
54  remove_packages(hass, coordinator.account_id, previous_tracking_numbers)
55  return
56  live_tracking_numbers = set(coordinator.data.live_packages.keys())
57 
58  new_tracking_numbers = live_tracking_numbers - previous_tracking_numbers
59  old_tracking_numbers = previous_tracking_numbers - live_tracking_numbers
60 
61  previous_tracking_numbers.update(live_tracking_numbers)
62 
63  packages_to_add = [
64  coordinator.data.live_packages[tracking_number]
65  for tracking_number in new_tracking_numbers
66  ]
67 
68  for package_data in coordinator.data.live_packages.values():
69  if (
70  package_data.status == VALUE_DELIVERED
71  and not coordinator.show_delivered
72  ):
73  old_tracking_numbers.add(package_data.tracking_number)
75  hass,
76  package_data.friendly_name,
77  package_data.tracking_number,
78  )
79 
80  remove_packages(hass, coordinator.account_id, old_tracking_numbers)
81 
84  coordinator,
85  package_data.tracking_number,
86  )
87  for package_data in packages_to_add
88  if not (
89  not coordinator.show_delivered and package_data.status == "Delivered"
90  )
91  )
92 
94  SeventeenTrackSummarySensor(status, coordinator)
95  for status, summary_data in coordinator.data.summary.items()
96  )
97 
98  if not config_entry.data.get(DEPRECATED_KEY):
99  deprecate_sensor_issue(hass, config_entry.entry_id)
100  _async_create_remove_entities()
101  config_entry.async_on_unload(
102  coordinator.async_add_listener(_async_create_remove_entities)
103  )
104 
105 
106 class SeventeenTrackSensor(CoordinatorEntity[SeventeenTrackCoordinator], SensorEntity):
107  """Define a 17Track sensor."""
108 
109  _attr_attribution = ATTRIBUTION
110  _attr_has_entity_name = True
111 
112  def __init__(self, coordinator: SeventeenTrackCoordinator) -> None:
113  """Initialize the sensor."""
114  super().__init__(coordinator)
115  self._attr_device_info_attr_device_info = DeviceInfo(
116  identifiers={(DOMAIN, coordinator.account_id)},
117  entry_type=DeviceEntryType.SERVICE,
118  name="17Track",
119  )
120 
121 
123  """Define a summary sensor."""
124 
125  _attr_native_unit_of_measurement = "packages"
126 
127  def __init__(
128  self,
129  status: str,
130  coordinator: SeventeenTrackCoordinator,
131  ) -> None:
132  """Initialize the sensor."""
133  super().__init__(coordinator)
134  self._status_status = status
135  self._attr_translation_key_attr_translation_key = status
136  self._attr_unique_id_attr_unique_id = f"summary_{coordinator.account_id}_{status}"
137 
138  @property
139  def available(self) -> bool:
140  """Return whether the entity is available."""
141  return self._status_status in self.coordinator.data.summary
142 
143  @property
144  def native_value(self) -> StateType:
145  """Return the state of the sensor."""
146  return self.coordinator.data.summary[self._status_status]["quantity"]
147 
148  # This has been deprecated in 2024.8, will be removed in 2025.2
149  @property
150  def extra_state_attributes(self) -> dict[str, Any] | None:
151  """Return the state attributes."""
152  packages = self.coordinator.data.summary[self._status_status]["packages"]
153  return {
154  ATTR_PACKAGES: [
155  {
156  ATTR_TRACKING_NUMBER: package.tracking_number,
157  ATTR_LOCATION: package.location,
158  ATTR_STATUS: package.status,
159  ATTR_TIMESTAMP: package.timestamp,
160  ATTR_INFO_TEXT: package.info_text,
161  ATTR_FRIENDLY_NAME: package.friendly_name,
162  }
163  for package in packages
164  ]
165  }
166 
167 
168 # The dynamic package sensors have been replaced by the seventeentrack.get_packages service
170  """Define an individual package sensor."""
171 
172  _attr_translation_key = "package"
173 
174  def __init__(
175  self,
176  coordinator: SeventeenTrackCoordinator,
177  tracking_number: str,
178  ) -> None:
179  """Initialize the sensor."""
180  super().__init__(coordinator)
181  self._tracking_number_tracking_number = tracking_number
182  self._previous_status_previous_status = coordinator.data.live_packages[tracking_number].status
183  self._attr_unique_id_attr_unique_id = UNIQUE_ID_TEMPLATE.format(
184  coordinator.account_id, tracking_number
185  )
186  package = coordinator.data.live_packages[tracking_number]
187  if not (name := package.friendly_name):
188  name = tracking_number
189  self._attr_translation_placeholders_attr_translation_placeholders = {"name": name}
190 
191  @property
192  def available(self) -> bool:
193  """Return whether the entity is available."""
194  return self._tracking_number_tracking_number in self.coordinator.data.live_packages
195 
196  @property
197  def native_value(self) -> StateType:
198  """Return the state."""
199  return self.coordinator.data.live_packages[self._tracking_number_tracking_number].status
200 
201  @property
202  def extra_state_attributes(self) -> dict[str, Any] | None:
203  """Return the state attributes."""
204  package = self.coordinator.data.live_packages[self._tracking_number_tracking_number]
205  return {
206  ATTR_DESTINATION_COUNTRY: package.destination_country,
207  ATTR_INFO_TEXT: package.info_text,
208  ATTR_TIMESTAMP: package.timestamp,
209  ATTR_LOCATION: package.location,
210  ATTR_ORIGIN_COUNTRY: package.origin_country,
211  ATTR_PACKAGE_TYPE: package.package_type,
212  ATTR_TRACKING_INFO_LANGUAGE: package.tracking_info_language,
213  ATTR_TRACKING_NUMBER: package.tracking_number,
214  }
215 
216 
217 def remove_packages(hass: HomeAssistant, account_id: str, packages: set[str]) -> None:
218  """Remove entity itself."""
219  reg = er.async_get(hass)
220  for package in packages:
221  entity_id = reg.async_get_entity_id(
222  "sensor",
223  "seventeentrack",
224  UNIQUE_ID_TEMPLATE.format(account_id, package),
225  )
226  if entity_id:
227  reg.async_remove(entity_id)
228 
229 
230 def notify_delivered(hass: HomeAssistant, friendly_name: str, tracking_number: str):
231  """Notify when package is delivered."""
232  LOGGER.debug("Package delivered: %s", tracking_number)
233 
234  identification = friendly_name if friendly_name else tracking_number
235  message = NOTIFICATION_DELIVERED_MESSAGE.format(identification, tracking_number)
236  title = NOTIFICATION_DELIVERED_TITLE.format(identification)
237  notification_id = NOTIFICATION_DELIVERED_TITLE.format(tracking_number)
238 
239  persistent_notification.create(
240  hass, message, title=title, notification_id=notification_id
241  )
242 
243 
244 @callback
245 def deprecate_sensor_issue(hass: HomeAssistant, entry_id: str) -> None:
246  """Ensure an issue is registered."""
247  ir.async_create_issue(
248  hass,
249  DOMAIN,
250  f"deprecate_sensor_{entry_id}",
251  breaks_in_ha_version="2025.2.0",
252  issue_domain=DOMAIN,
253  is_fixable=True,
254  is_persistent=True,
255  translation_key="deprecate_sensor",
256  severity=ir.IssueSeverity.WARNING,
257  data={"entry_id": entry_id},
258  )
None __init__(self, SeventeenTrackCoordinator coordinator, str tracking_number)
Definition: sensor.py:178
None __init__(self, SeventeenTrackCoordinator coordinator)
Definition: sensor.py:112
None __init__(self, str status, SeventeenTrackCoordinator coordinator)
Definition: sensor.py:131
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:44
None remove_packages(HomeAssistant hass, str account_id, set[str] packages)
Definition: sensor.py:217
None deprecate_sensor_issue(HomeAssistant hass, str entry_id)
Definition: sensor.py:245
def notify_delivered(HomeAssistant hass, str friendly_name, str tracking_number)
Definition: sensor.py:230