test_sensors.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. """Unit tests for sensor property logic.
  2. Bypasses HA entity setup by using object.__new__ and injecting a mock
  3. coordinator — valid because every tested property only reads self.coordinator.data.
  4. """
  5. from datetime import date, datetime, time, timezone
  6. from unittest.mock import MagicMock
  7. import pytest
  8. from custom_components.room_cal_ha.coordinator import CoordinatorData
  9. from custom_components.room_cal_ha.models import ApiStatusData, ReservationData
  10. from custom_components.room_cal_ha.sensor import (
  11. ApiCheckedAtSensor,
  12. ApiStatusSensor,
  13. LastRefreshSensor,
  14. ReservationDateTimeSensor,
  15. ReservationStrSensor,
  16. ReservationTimeSensor,
  17. WiFiKeySensor,
  18. )
  19. from custom_components.room_cal_ha.const import STORE_CURRENT, STORE_NEXT
  20. # ---------------------------------------------------------------------------
  21. # Helpers
  22. # ---------------------------------------------------------------------------
  23. def _coordinator(
  24. current=None,
  25. nxt=None,
  26. current_ts=None,
  27. next_ts=None,
  28. api_status=None,
  29. ):
  30. coord = MagicMock()
  31. coord.data = CoordinatorData(
  32. current=current or ReservationData(),
  33. next=nxt or ReservationData(),
  34. current_last_refresh=current_ts,
  35. next_last_refresh=next_ts,
  36. api_status=api_status,
  37. )
  38. return coord
  39. def _sensor(cls, coordinator, slot=STORE_CURRENT, field="guest_name", **extra):
  40. """Create a sensor instance without running __init__."""
  41. s = object.__new__(cls)
  42. s.coordinator = coordinator
  43. s._slot = slot
  44. s._field = field
  45. for k, v in extra.items():
  46. setattr(s, k, v)
  47. return s
  48. SAMPLE = ReservationData(
  49. guest_name="Alice",
  50. check_in_date=date(2026, 7, 1),
  51. check_in_time=time(14, 0),
  52. check_out_date=date(2026, 7, 5),
  53. check_out_time=time(12, 0),
  54. wifi_ssid="MyNet",
  55. wifi_key="hunter2",
  56. door_code="0472",
  57. )
  58. # ---------------------------------------------------------------------------
  59. # ReservationStrSensor
  60. # ---------------------------------------------------------------------------
  61. def test_str_sensor_returns_field_value():
  62. coord = _coordinator(current=SAMPLE)
  63. s = _sensor(ReservationStrSensor, coord, field="guest_name")
  64. assert s.native_value == "Alice"
  65. def test_str_sensor_reads_next_slot():
  66. nxt = ReservationData(guest_name="Bob")
  67. coord = _coordinator(nxt=nxt)
  68. s = _sensor(ReservationStrSensor, coord, slot=STORE_NEXT, field="guest_name")
  69. assert s.native_value == "Bob"
  70. def test_str_sensor_none_when_field_is_none():
  71. res = ReservationData(guest_name=None)
  72. coord = _coordinator(current=res)
  73. s = _sensor(ReservationStrSensor, coord, field="guest_name")
  74. assert s.native_value is None
  75. # ---------------------------------------------------------------------------
  76. # ReservationTimeSensor
  77. # ---------------------------------------------------------------------------
  78. def test_time_sensor_returns_iso_string():
  79. coord = _coordinator(current=SAMPLE)
  80. s = _sensor(ReservationTimeSensor, coord, field="check_in_time")
  81. assert s.native_value == "14:00:00"
  82. def test_time_sensor_none_when_time_is_none():
  83. res = ReservationData(check_in_time=None)
  84. coord = _coordinator(current=res)
  85. s = _sensor(ReservationTimeSensor, coord, field="check_in_time")
  86. assert s.native_value is None
  87. # ---------------------------------------------------------------------------
  88. # ReservationDateTimeSensor
  89. # ---------------------------------------------------------------------------
  90. def test_datetime_sensor_native_value_is_date():
  91. coord = _coordinator(current=SAMPLE)
  92. s = _sensor(ReservationDateTimeSensor, coord, field="check_in_date")
  93. s._time_field = "check_in_time"
  94. assert s.native_value == date(2026, 7, 1)
  95. def test_datetime_sensor_attrs_contain_expected_keys():
  96. coord = _coordinator(current=SAMPLE)
  97. s = _sensor(ReservationDateTimeSensor, coord, field="check_in_date")
  98. s._time_field = "check_in_time"
  99. attrs = s.extra_state_attributes
  100. assert attrs is not None
  101. for key in ("datetime", "date", "weekday", "month", "time_abbr", "hour"):
  102. assert key in attrs, f"missing key: {key}"
  103. def test_datetime_sensor_attrs_none_when_date_missing():
  104. res = ReservationData(check_in_date=None, check_in_time=time(14, 0))
  105. coord = _coordinator(current=res)
  106. s = _sensor(ReservationDateTimeSensor, coord, field="check_in_date")
  107. s._time_field = "check_in_time"
  108. assert s.extra_state_attributes is None
  109. def test_datetime_sensor_attrs_none_when_time_missing():
  110. res = ReservationData(check_in_date=date(2026, 7, 1), check_in_time=None)
  111. coord = _coordinator(current=res)
  112. s = _sensor(ReservationDateTimeSensor, coord, field="check_in_date")
  113. s._time_field = "check_in_time"
  114. assert s.extra_state_attributes is None
  115. # ---------------------------------------------------------------------------
  116. # WiFiKeySensor
  117. # ---------------------------------------------------------------------------
  118. def test_wifi_key_sensor_native_value():
  119. coord = _coordinator(current=SAMPLE)
  120. s = _sensor(WiFiKeySensor, coord, field="wifi_key")
  121. assert s.native_value == "hunter2"
  122. def test_wifi_key_sensor_qr_attribute():
  123. coord = _coordinator(current=SAMPLE)
  124. s = _sensor(WiFiKeySensor, coord, field="wifi_key")
  125. attrs = s.extra_state_attributes
  126. assert attrs is not None
  127. assert attrs["qr"] == "WIFI:S:MyNet;T:WPA;P:hunter2;;"
  128. def test_wifi_key_sensor_no_attrs_when_ssid_missing():
  129. res = ReservationData(wifi_ssid=None, wifi_key="hunter2")
  130. coord = _coordinator(current=res)
  131. s = _sensor(WiFiKeySensor, coord, field="wifi_key")
  132. assert s.extra_state_attributes is None
  133. def test_wifi_key_sensor_no_attrs_when_key_missing():
  134. res = ReservationData(wifi_ssid="MyNet", wifi_key=None)
  135. coord = _coordinator(current=res)
  136. s = _sensor(WiFiKeySensor, coord, field="wifi_key")
  137. assert s.extra_state_attributes is None
  138. # ---------------------------------------------------------------------------
  139. # LastRefreshSensor
  140. # ---------------------------------------------------------------------------
  141. def test_last_refresh_sensor_current():
  142. ts = datetime(2026, 6, 22, 10, 0, tzinfo=timezone.utc)
  143. coord = _coordinator(current_ts=ts)
  144. s = _sensor(LastRefreshSensor, coord)
  145. s._slot = STORE_CURRENT
  146. assert s.native_value == ts
  147. def test_last_refresh_sensor_next():
  148. ts = datetime(2026, 6, 22, 12, 0, tzinfo=timezone.utc)
  149. coord = _coordinator(next_ts=ts)
  150. s = _sensor(LastRefreshSensor, coord)
  151. s._slot = STORE_NEXT
  152. assert s.native_value == ts
  153. def test_last_refresh_sensor_none_before_any_fetch():
  154. coord = _coordinator()
  155. s = _sensor(LastRefreshSensor, coord)
  156. s._slot = STORE_CURRENT
  157. assert s.native_value is None
  158. # ---------------------------------------------------------------------------
  159. # ApiStatusSensor
  160. # ---------------------------------------------------------------------------
  161. def test_api_status_sensor_native_value():
  162. status = ApiStatusData(status="ok")
  163. coord = _coordinator(api_status=status)
  164. s = object.__new__(ApiStatusSensor)
  165. s.coordinator = coord
  166. assert s.native_value == "ok"
  167. def test_api_status_sensor_none_when_no_status():
  168. coord = _coordinator(api_status=None)
  169. s = object.__new__(ApiStatusSensor)
  170. s.coordinator = coord
  171. assert s.native_value is None
  172. def test_api_status_sensor_attrs_include_checks_and_stale():
  173. checks = {"caldav_connection": {"ok": True}, "calendar": {"ok": False, "reason": "not found"}}
  174. status = ApiStatusData(status="degraded", checks=checks, stale=True)
  175. coord = _coordinator(api_status=status)
  176. s = object.__new__(ApiStatusSensor)
  177. s.coordinator = coord
  178. attrs = s.extra_state_attributes
  179. assert attrs["caldav_connection"] == {"ok": True}
  180. assert attrs["calendar"] == {"ok": False, "reason": "not found"}
  181. assert attrs["stale"] is True
  182. def test_api_status_sensor_attrs_include_checked_at_when_present():
  183. ts = datetime(2026, 6, 22, 10, 0, tzinfo=timezone.utc)
  184. status = ApiStatusData(status="ok", checked_at=ts)
  185. coord = _coordinator(api_status=status)
  186. s = object.__new__(ApiStatusSensor)
  187. s.coordinator = coord
  188. attrs = s.extra_state_attributes
  189. assert "checked_at" in attrs
  190. assert attrs["checked_at"] == ts.isoformat()
  191. def test_api_status_sensor_attrs_none_when_no_status():
  192. coord = _coordinator(api_status=None)
  193. s = object.__new__(ApiStatusSensor)
  194. s.coordinator = coord
  195. assert s.extra_state_attributes is None
  196. # ---------------------------------------------------------------------------
  197. # ApiCheckedAtSensor
  198. # ---------------------------------------------------------------------------
  199. def test_api_checked_at_sensor_returns_timestamp():
  200. ts = datetime(2026, 6, 22, 10, 0, tzinfo=timezone.utc)
  201. status = ApiStatusData(checked_at=ts)
  202. coord = _coordinator(api_status=status)
  203. s = object.__new__(ApiCheckedAtSensor)
  204. s.coordinator = coord
  205. assert s.native_value == ts
  206. def test_api_checked_at_sensor_none_when_no_status():
  207. coord = _coordinator(api_status=None)
  208. s = object.__new__(ApiCheckedAtSensor)
  209. s.coordinator = coord
  210. assert s.native_value is None
  211. def test_api_checked_at_sensor_none_when_checked_at_not_set():
  212. status = ApiStatusData(status="starting")
  213. coord = _coordinator(api_status=status)
  214. s = object.__new__(ApiCheckedAtSensor)
  215. s.coordinator = coord
  216. assert s.native_value is None