"""Unit tests for LocalThingsSensor's debounce gate (finish_time churn).""" from __future__ import annotations from datetime import datetime, timedelta, timezone from custom_components.localthings.const import CONF_FINISH_TIME_DEBOUNCE_MINUTES from custom_components.localthings.registry.capabilities.operational import OPERATIONAL_STATE from custom_components.localthings.registry.discovery import BoundEntity from custom_components.localthings.sensor import LocalThingsSensor _FINISH_TIME_DESC = next(e for e in OPERATIONAL_STATE.entities if e.key == 'finish_time') class _FakeConfigEntry: def __init__(self, options): self.options = options class _FakeCoordinator: """Just enough surface for LocalThingsEntity/LocalThingsSensor.""" def __init__(self, threshold_minutes): self.device_serial = 'TEST-SERIAL' self.config_entry = _FakeConfigEntry({ CONF_FINISH_TIME_DEBOUNCE_MINUTES: threshold_minutes, }) self.data: dict = {} def _sensor(threshold_minutes=3): coordinator = _FakeCoordinator(threshold_minutes) bound = BoundEntity( href=OPERATIONAL_STATE.href, capability=OPERATIONAL_STATE, desc=_FINISH_TIME_DESC, ) sensor = LocalThingsSensor(coordinator, bound) return sensor, coordinator def test_small_change_is_suppressed(): sensor, coordinator = _sensor(threshold_minutes=3) base = datetime(2026, 7, 31, 17, 0, tzinfo=timezone.utc) coordinator.data = {'finish_time': base} assert sensor.native_value == base coordinator.data = {'finish_time': base + timedelta(minutes=1)} assert sensor.native_value == base, "a 1-minute wobble should be held back" def test_change_past_threshold_is_reported(): sensor, coordinator = _sensor(threshold_minutes=3) base = datetime(2026, 7, 31, 17, 0, tzinfo=timezone.utc) coordinator.data = {'finish_time': base} assert sensor.native_value == base new = base + timedelta(minutes=5) coordinator.data = {'finish_time': new} assert sensor.native_value == new def test_zero_threshold_disables_debounce(): sensor, coordinator = _sensor(threshold_minutes=0) base = datetime(2026, 7, 31, 17, 0, tzinfo=timezone.utc) coordinator.data = {'finish_time': base} assert sensor.native_value == base new = base + timedelta(seconds=1) coordinator.data = {'finish_time': new} assert sensor.native_value == new def test_cycle_end_passes_through_immediately(): """A cycle ending (finish_time -> None) must never be held back.""" sensor, coordinator = _sensor(threshold_minutes=3) base = datetime(2026, 7, 31, 17, 0, tzinfo=timezone.utc) coordinator.data = {'finish_time': base} assert sensor.native_value == base coordinator.data = {'finish_time': None} assert sensor.native_value is None def test_non_debounced_sensor_is_unaffected(): """A SensorDesc without debounce=True reads straight through, unchanged.""" machine_state_desc = next( e for e in OPERATIONAL_STATE.entities if e.key == 'machine_state' ) coordinator = _FakeCoordinator(threshold_minutes=3) bound = BoundEntity( href=OPERATIONAL_STATE.href, capability=OPERATIONAL_STATE, desc=machine_state_desc, ) sensor = LocalThingsSensor(coordinator, bound) coordinator.data = {'machine_state': 'active'} assert sensor.native_value == 'active' coordinator.data = {'machine_state': 'idle'} assert sensor.native_value == 'idle'