test_binary_sensor.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. """Unit tests for StaleSensor.
  2. Uses object.__new__ to bypass HA entity setup — valid because is_on only
  3. reads entry.options, coordinator.data timestamps, and dt_util.utcnow().
  4. """
  5. from datetime import datetime, timedelta, timezone
  6. from unittest.mock import MagicMock, patch
  7. import pytest
  8. from custom_components.room_cal_ha.binary_sensor import StaleSensor
  9. from custom_components.room_cal_ha.coordinator import CoordinatorData
  10. from custom_components.room_cal_ha.const import (
  11. CONF_REFRESH_INTERVAL,
  12. CONF_SYNC_MODE,
  13. DEFAULT_REFRESH_INTERVAL,
  14. SYNC_MODE_CALDAV,
  15. SYNC_MODE_MANUAL,
  16. )
  17. from custom_components.room_cal_ha.models import ReservationData
  18. _NOW = datetime(2026, 6, 22, 12, 0, 0, tzinfo=timezone.utc)
  19. def _sensor(sync_mode=SYNC_MODE_CALDAV, interval=DEFAULT_REFRESH_INTERVAL, current_ts=None, next_ts=None):
  20. s = object.__new__(StaleSensor)
  21. s._entry = MagicMock()
  22. s._entry.options = {CONF_SYNC_MODE: sync_mode, CONF_REFRESH_INTERVAL: interval}
  23. coord = MagicMock()
  24. coord.data = CoordinatorData(
  25. active=ReservationData(),
  26. current=ReservationData(),
  27. next=ReservationData(),
  28. current_last_refresh=current_ts,
  29. next_last_refresh=next_ts,
  30. )
  31. s.coordinator = coord
  32. return s
  33. # ---------------------------------------------------------------------------
  34. # Manual mode — always None
  35. # ---------------------------------------------------------------------------
  36. def test_stale_sensor_manual_mode_returns_none():
  37. s = _sensor(sync_mode=SYNC_MODE_MANUAL)
  38. with patch("custom_components.room_cal_ha.binary_sensor.dt_util.utcnow", return_value=_NOW):
  39. assert s.is_on is None
  40. # ---------------------------------------------------------------------------
  41. # CalDAV mode — fresh timestamps
  42. # ---------------------------------------------------------------------------
  43. def test_stale_sensor_fresh_timestamps_returns_false():
  44. recent = _NOW - timedelta(minutes=5)
  45. s = _sensor(current_ts=recent, next_ts=recent, interval=15)
  46. with patch("custom_components.room_cal_ha.binary_sensor.dt_util.utcnow", return_value=_NOW):
  47. assert s.is_on is False
  48. # ---------------------------------------------------------------------------
  49. # CalDAV mode — stale / missing timestamps
  50. # ---------------------------------------------------------------------------
  51. def test_stale_sensor_none_current_ts_returns_true():
  52. recent = _NOW - timedelta(minutes=5)
  53. s = _sensor(current_ts=None, next_ts=recent, interval=15)
  54. with patch("custom_components.room_cal_ha.binary_sensor.dt_util.utcnow", return_value=_NOW):
  55. assert s.is_on is True
  56. def test_stale_sensor_none_next_ts_returns_true():
  57. recent = _NOW - timedelta(minutes=5)
  58. s = _sensor(current_ts=recent, next_ts=None, interval=15)
  59. with patch("custom_components.room_cal_ha.binary_sensor.dt_util.utcnow", return_value=_NOW):
  60. assert s.is_on is True
  61. def test_stale_sensor_both_ts_none_returns_true():
  62. s = _sensor(current_ts=None, next_ts=None, interval=15)
  63. with patch("custom_components.room_cal_ha.binary_sensor.dt_util.utcnow", return_value=_NOW):
  64. assert s.is_on is True
  65. def test_stale_sensor_expired_current_ts_returns_true():
  66. # interval=15 → threshold=30 min; set current 31 min ago
  67. stale = _NOW - timedelta(minutes=31)
  68. fresh = _NOW - timedelta(minutes=5)
  69. s = _sensor(current_ts=stale, next_ts=fresh, interval=15)
  70. with patch("custom_components.room_cal_ha.binary_sensor.dt_util.utcnow", return_value=_NOW):
  71. assert s.is_on is True
  72. def test_stale_sensor_exactly_at_threshold_is_not_stale():
  73. # Exactly 2x interval is NOT stale (uses strict >)
  74. threshold = _NOW - timedelta(minutes=30)
  75. s = _sensor(current_ts=threshold, next_ts=threshold, interval=15)
  76. with patch("custom_components.room_cal_ha.binary_sensor.dt_util.utcnow", return_value=_NOW):
  77. assert s.is_on is False
  78. def test_stale_sensor_custom_interval():
  79. # interval=5 → threshold=10 min; set both 9 min ago → fresh
  80. recent = _NOW - timedelta(minutes=9)
  81. s = _sensor(current_ts=recent, next_ts=recent, interval=5)
  82. with patch("custom_components.room_cal_ha.binary_sensor.dt_util.utcnow", return_value=_NOW):
  83. assert s.is_on is False