Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Get WHOIS information for a given host."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from dataclasses import dataclass
7 from datetime import UTC, datetime
8 from typing import cast
9 
10 from whois import Domain
11 
13  SensorDeviceClass,
14  SensorEntity,
15  SensorEntityDescription,
16 )
17 from homeassistant.config_entries import ConfigEntry
18 from homeassistant.const import CONF_DOMAIN, EntityCategory, UnitOfTime
19 from homeassistant.core import HomeAssistant
20 from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
21 from homeassistant.helpers.entity_platform import AddEntitiesCallback
23  CoordinatorEntity,
24  DataUpdateCoordinator,
25 )
26 from homeassistant.util import dt as dt_util
27 
28 from .const import ATTR_EXPIRES, ATTR_NAME_SERVERS, ATTR_REGISTRAR, ATTR_UPDATED, DOMAIN
29 
30 
31 @dataclass(frozen=True, kw_only=True)
33  """Describes a Whois sensor entity."""
34 
35  value_fn: Callable[[Domain], datetime | int | str | None]
36 
37 
38 def _days_until_expiration(domain: Domain) -> int | None:
39  """Calculate days left until domain expires."""
40  if domain.expiration_date is None:
41  return None
42  # We need to cast here, as (unlike Pyright) mypy isn't able to determine the type.
43  return cast(
44  int,
45  (domain.expiration_date - dt_util.utcnow().replace(tzinfo=None)).days,
46  )
47 
48 
49 def _ensure_timezone(timestamp: datetime | None) -> datetime | None:
50  """Calculate days left until domain expires."""
51  if timestamp is None:
52  return None
53 
54  # If timezone info isn't provided by the Whois, assume UTC.
55  if timestamp.tzinfo is None:
56  return timestamp.replace(tzinfo=UTC)
57 
58  return timestamp
59 
60 
61 SENSORS: tuple[WhoisSensorEntityDescription, ...] = (
63  key="admin",
64  translation_key="admin",
65  entity_category=EntityCategory.DIAGNOSTIC,
66  entity_registry_enabled_default=False,
67  value_fn=lambda domain: getattr(domain, "admin", None),
68  ),
70  key="creation_date",
71  translation_key="creation_date",
72  device_class=SensorDeviceClass.TIMESTAMP,
73  entity_category=EntityCategory.DIAGNOSTIC,
74  value_fn=lambda domain: _ensure_timezone(domain.creation_date),
75  ),
77  key="days_until_expiration",
78  translation_key="days_until_expiration",
79  native_unit_of_measurement=UnitOfTime.DAYS,
80  value_fn=_days_until_expiration,
81  ),
83  key="expiration_date",
84  translation_key="expiration_date",
85  device_class=SensorDeviceClass.TIMESTAMP,
86  entity_category=EntityCategory.DIAGNOSTIC,
87  value_fn=lambda domain: _ensure_timezone(domain.expiration_date),
88  ),
90  key="last_updated",
91  translation_key="last_updated",
92  device_class=SensorDeviceClass.TIMESTAMP,
93  entity_category=EntityCategory.DIAGNOSTIC,
94  value_fn=lambda domain: _ensure_timezone(domain.last_updated),
95  ),
97  key="owner",
98  translation_key="owner",
99  entity_category=EntityCategory.DIAGNOSTIC,
100  entity_registry_enabled_default=False,
101  value_fn=lambda domain: getattr(domain, "owner", None),
102  ),
104  key="registrant",
105  translation_key="registrant",
106  entity_category=EntityCategory.DIAGNOSTIC,
107  entity_registry_enabled_default=False,
108  value_fn=lambda domain: getattr(domain, "registrant", None),
109  ),
111  key="registrar",
112  translation_key="registrar",
113  entity_category=EntityCategory.DIAGNOSTIC,
114  entity_registry_enabled_default=False,
115  value_fn=lambda domain: domain.registrar if domain.registrar else None,
116  ),
118  key="reseller",
119  translation_key="reseller",
120  entity_category=EntityCategory.DIAGNOSTIC,
121  entity_registry_enabled_default=False,
122  value_fn=lambda domain: getattr(domain, "reseller", None),
123  ),
124 )
125 
126 
128  hass: HomeAssistant,
129  entry: ConfigEntry,
130  async_add_entities: AddEntitiesCallback,
131 ) -> None:
132  """Set up the platform from config_entry."""
133  coordinator: DataUpdateCoordinator[Domain | None] = hass.data[DOMAIN][
134  entry.entry_id
135  ]
137  [
139  coordinator=coordinator,
140  description=description,
141  domain=entry.data[CONF_DOMAIN],
142  )
143  for description in SENSORS
144  ],
145  )
146 
147 
149  CoordinatorEntity[DataUpdateCoordinator[Domain | None]], SensorEntity
150 ):
151  """Implementation of a WHOIS sensor."""
152 
153  entity_description: WhoisSensorEntityDescription
154  _attr_has_entity_name = True
155 
156  def __init__(
157  self,
158  coordinator: DataUpdateCoordinator[Domain | None],
159  description: WhoisSensorEntityDescription,
160  domain: str,
161  ) -> None:
162  """Initialize the sensor."""
163  super().__init__(coordinator=coordinator)
164  self.entity_descriptionentity_description = description
165  self._attr_unique_id_attr_unique_id = f"{domain}_{description.key}"
166  self._attr_device_info_attr_device_info = DeviceInfo(
167  identifiers={(DOMAIN, domain)},
168  name=domain,
169  entry_type=DeviceEntryType.SERVICE,
170  )
171  self._domain_domain = domain
172 
173  @property
174  def native_value(self) -> datetime | int | str | None:
175  """Return the state of the sensor."""
176  if self.coordinator.data is None:
177  return None
178  return self.entity_descriptionentity_description.value_fn(self.coordinator.data)
179 
180  @property
181  def extra_state_attributes(self) -> dict[str, int | float | None] | None:
182  """Return the state attributes of the monitored installation."""
183 
184  # Only add attributes to the original sensor
185  if self.entity_descriptionentity_description.key != "days_until_expiration":
186  return None
187 
188  if self.coordinator.data is None:
189  return None
190 
191  attrs = {}
192  if expiration_date := self.coordinator.data.expiration_date:
193  attrs[ATTR_EXPIRES] = expiration_date.isoformat()
194 
195  if name_servers := self.coordinator.data.name_servers:
196  attrs[ATTR_NAME_SERVERS] = " ".join(name_servers)
197 
198  if last_updated := self.coordinator.data.last_updated:
199  attrs[ATTR_UPDATED] = last_updated.isoformat()
200 
201  if registrar := self.coordinator.data.registrar:
202  attrs[ATTR_REGISTRAR] = registrar
203 
204  if not attrs:
205  return None
206 
207  return attrs
dict[str, int|float|None]|None extra_state_attributes(self)
Definition: sensor.py:181
None __init__(self, DataUpdateCoordinator[Domain|None] coordinator, WhoisSensorEntityDescription description, str domain)
Definition: sensor.py:161
None async_setup_entry(HomeAssistant hass, ConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:131
int|None _days_until_expiration(Domain domain)
Definition: sensor.py:38
datetime|None _ensure_timezone(datetime|None timestamp)
Definition: sensor.py:49