sensor.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. from __future__ import annotations
  2. import re
  3. from datetime import date, datetime, time
  4. from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
  5. from homeassistant.config_entries import ConfigEntry
  6. from homeassistant.core import HomeAssistant
  7. from homeassistant.helpers.device_registry import DeviceInfo
  8. from homeassistant.helpers.entity_platform import AddEntitiesCallback
  9. from homeassistant.helpers.update_coordinator import CoordinatorEntity
  10. from .const import (
  11. CONF_API_BASE_URL,
  12. CONF_CALDAV_URL,
  13. CONF_ROOM_NAME,
  14. CONF_SYNC_MODE,
  15. DOMAIN,
  16. STORE_CURRENT,
  17. STORE_NEXT,
  18. SYNC_MODE_API,
  19. SYNC_MODE_CALDAV,
  20. SYNC_MODE_MANUAL,
  21. )
  22. from .coordinator import CoordinatorData, RoomCalCoordinator
  23. from .models import ReservationData
  24. async def async_setup_entry(
  25. hass: HomeAssistant,
  26. entry: ConfigEntry,
  27. async_add_entities: AddEntitiesCallback,
  28. ) -> None:
  29. coordinator: RoomCalCoordinator = hass.data[DOMAIN][entry.entry_id]["coordinator"]
  30. room_name = entry.data.get(CONF_ROOM_NAME, entry.title)
  31. room_state_device = DeviceInfo(
  32. identifiers={(DOMAIN, f"{entry.entry_id}_room_state")},
  33. name=f"{room_name} Room State",
  34. )
  35. entities: list[SensorEntity] = [
  36. SyncSourceSensor(entry, room_name, room_state_device),
  37. ]
  38. if entry.options.get(CONF_SYNC_MODE) == SYNC_MODE_API:
  39. entities.append(ApiHealthSensor(coordinator, entry, room_state_device))
  40. for slot, device_name in (
  41. (STORE_CURRENT, f"{room_name} Current"),
  42. (STORE_NEXT, f"{room_name} Next"),
  43. ):
  44. device_id = f"{entry.entry_id}_{slot}"
  45. device_info = DeviceInfo(
  46. identifiers={(DOMAIN, device_id)},
  47. name=device_name,
  48. )
  49. entities += [
  50. GuestNameSensor(coordinator, entry, slot, device_info, "Guest Name", "guest_name", "mdi:account-convert"),
  51. ReservationDateTimeSensor(coordinator, entry, slot, device_info, "Arrival", "check_in_date", "check_in_time", "mdi:calendar-start"),
  52. ReservationTimeSensor(coordinator, entry, slot, device_info, "Check In", "check_in_time", "mdi:timer-check-outline"),
  53. ReservationDateTimeSensor(coordinator, entry, slot, device_info, "Departure", "check_out_date", "check_out_time", "mdi:calendar-end"),
  54. ReservationTimeSensor(coordinator, entry, slot, device_info, "Check Out", "check_out_time", "mdi:timer-cancel-outline"),
  55. ReservationStrSensor(coordinator, entry, slot, device_info, "WiFi SSID", "wifi_ssid", "mdi:wifi"),
  56. WiFiKeySensor(coordinator, entry, slot, device_info),
  57. ReservationStrSensor(coordinator, entry, slot, device_info, "Door Code", "door_code", "mdi:door-closed-lock"),
  58. LastRefreshSensor(coordinator, entry, slot, device_info),
  59. ]
  60. async_add_entities(entities)
  61. class SyncSourceSensor(SensorEntity):
  62. _attr_has_entity_name = True
  63. _attr_name = "Sync Source"
  64. _attr_icon = "mdi:sync"
  65. def __init__(self, entry: ConfigEntry, room_name: str, device_info: DeviceInfo) -> None:
  66. self._entry = entry
  67. self._attr_unique_id = f"{entry.entry_id}_sync_source"
  68. self._attr_device_info = device_info
  69. @property
  70. def native_value(self) -> str:
  71. return self._entry.options.get(CONF_SYNC_MODE, SYNC_MODE_MANUAL)
  72. @property
  73. def extra_state_attributes(self) -> dict[str, str | None]:
  74. mode = self._entry.options.get(CONF_SYNC_MODE, SYNC_MODE_MANUAL)
  75. if mode == SYNC_MODE_CALDAV:
  76. url = self._entry.options.get(CONF_CALDAV_URL)
  77. elif mode == SYNC_MODE_API:
  78. url = self._entry.options.get(CONF_API_BASE_URL)
  79. else:
  80. url = None
  81. return {"server_url": url}
  82. class _ReservationSensor(CoordinatorEntity[RoomCalCoordinator], SensorEntity):
  83. _attr_has_entity_name = True
  84. def __init__(
  85. self,
  86. coordinator: RoomCalCoordinator,
  87. entry: ConfigEntry,
  88. slot: str,
  89. device_info: DeviceInfo,
  90. name: str,
  91. field: str,
  92. icon: str | None = None,
  93. ) -> None:
  94. super().__init__(coordinator)
  95. self._slot = slot
  96. self._field = field
  97. self._attr_name = name
  98. self._attr_unique_id = f"{entry.entry_id}_{slot}_{field}"
  99. self._attr_device_info = device_info
  100. self._attr_icon = icon
  101. def _reservation(self) -> ReservationData:
  102. data: CoordinatorData = self.coordinator.data
  103. return data.current if self._slot == STORE_CURRENT else data.next
  104. class ReservationStrSensor(_ReservationSensor):
  105. @property
  106. def native_value(self) -> str | None:
  107. return getattr(self._reservation(), self._field)
  108. class GuestNameSensor(ReservationStrSensor):
  109. @property
  110. def extra_state_attributes(self) -> dict[str, str | None] | None:
  111. full: str | None = self._reservation().guest_name
  112. if full is None:
  113. return None
  114. parts = full.rsplit(" ", 1)
  115. first_name = parts[0]
  116. last_name = parts[1] if len(parts) > 1 else None
  117. first_abbr = first_name[0].upper() if first_name else None
  118. last_abbr = last_name[0].upper() if last_name else None
  119. public = f"{first_name} {last_abbr}." if last_abbr else full
  120. return {
  121. "first_name": first_name,
  122. "first_abbr": first_abbr,
  123. "last_name": last_name,
  124. "last_abbr": last_abbr,
  125. "public": public,
  126. }
  127. class WiFiKeySensor(_ReservationSensor):
  128. def __init__(self, coordinator, entry, slot, device_info) -> None:
  129. super().__init__(coordinator, entry, slot, device_info, "WiFi Key", "wifi_key", "mdi:wifi-lock")
  130. @property
  131. def native_value(self) -> str | None:
  132. return self._reservation().wifi_key
  133. @property
  134. def extra_state_attributes(self) -> dict[str, str] | None:
  135. res = self._reservation()
  136. if res.wifi_ssid is None or res.wifi_key is None:
  137. return None
  138. return {"qr": f"WIFI:S:{res.wifi_ssid};T:WPA;P:{res.wifi_key};;"}
  139. class ReservationTimeSensor(_ReservationSensor):
  140. """Time sensor. HA has no TIME device class for sensors; we expose as string ISO."""
  141. @property
  142. def native_value(self) -> str | None:
  143. val: time | None = getattr(self._reservation(), self._field)
  144. return val.isoformat() if val is not None else None
  145. def _dt_attrs(dt: datetime) -> dict[str, str]:
  146. return {
  147. "datetime": dt.isoformat(),
  148. "datetime_abbr": dt.strftime("%Y-%m-%d @ %-I %p"),
  149. "date": dt.strftime("%Y-%m-%d"),
  150. "date_weekday": dt.strftime("%a, %b %-d"),
  151. "datetime_weekday": dt.strftime("%a, %b %-d @ %-I %p"),
  152. "time": dt.strftime("%H:%M:%S"),
  153. "time_abbr": dt.strftime("%H:%M"),
  154. "hour": re.sub("^0", " ", dt.strftime("%I %p")),
  155. "weekday": dt.strftime("%A"),
  156. "weekday_abbr": dt.strftime("%a"),
  157. "month": dt.strftime("%B"),
  158. "month_abbr": dt.strftime("%b"),
  159. }
  160. class ReservationDateTimeSensor(_ReservationSensor):
  161. """Date sensor that also exposes combined datetime attributes."""
  162. _attr_device_class = SensorDeviceClass.DATE
  163. def __init__(
  164. self,
  165. coordinator: RoomCalCoordinator,
  166. entry: ConfigEntry,
  167. slot: str,
  168. device_info: DeviceInfo,
  169. name: str,
  170. field: str,
  171. time_field: str,
  172. icon: str | None = None,
  173. ) -> None:
  174. super().__init__(coordinator, entry, slot, device_info, name, field, icon)
  175. self._time_field = time_field
  176. @property
  177. def native_value(self) -> date | None:
  178. return getattr(self._reservation(), self._field)
  179. @property
  180. def extra_state_attributes(self) -> dict[str, str] | None:
  181. res = self._reservation()
  182. d: date | None = getattr(res, self._field)
  183. t: time | None = getattr(res, self._time_field)
  184. if d is None or t is None:
  185. return None
  186. return _dt_attrs(datetime.combine(d, t))
  187. class LastRefreshSensor(CoordinatorEntity[RoomCalCoordinator], SensorEntity):
  188. _attr_has_entity_name = True
  189. _attr_name = "Last Refresh"
  190. _attr_device_class = SensorDeviceClass.TIMESTAMP
  191. _attr_icon = "mdi:timer-sync"
  192. def __init__(
  193. self,
  194. coordinator: RoomCalCoordinator,
  195. entry: ConfigEntry,
  196. slot: str,
  197. device_info: DeviceInfo,
  198. ) -> None:
  199. super().__init__(coordinator)
  200. self._slot = slot
  201. self._attr_unique_id = f"{entry.entry_id}_{slot}_last_refresh"
  202. self._attr_device_info = device_info
  203. @property
  204. def native_value(self) -> datetime | None:
  205. data: CoordinatorData = self.coordinator.data
  206. return (
  207. data.current_last_refresh
  208. if self._slot == STORE_CURRENT
  209. else data.next_last_refresh
  210. )
  211. class ApiHealthSensor(CoordinatorEntity[RoomCalCoordinator], SensorEntity):
  212. _attr_has_entity_name = True
  213. _attr_name = "API Health"
  214. _attr_icon = "mdi:api"
  215. def __init__(
  216. self,
  217. coordinator: RoomCalCoordinator,
  218. entry: ConfigEntry,
  219. device_info: DeviceInfo,
  220. ) -> None:
  221. super().__init__(coordinator)
  222. self._attr_unique_id = f"{entry.entry_id}_api_health"
  223. self._attr_device_info = device_info
  224. @property
  225. def native_value(self) -> str | None:
  226. api_status = self.coordinator.data.api_status
  227. if api_status is None:
  228. return None
  229. return "OK" if api_status.status == "OK" else "Degraded"
  230. @property
  231. def extra_state_attributes(self) -> dict | None:
  232. api_status = self.coordinator.data.api_status
  233. if api_status is None or not isinstance(api_status.services, dict) or not api_status.services:
  234. return None
  235. return dict(api_status.services)