Prechádzať zdrojové kódy

Add Override Empty button

Starts an override with an empty/defaults reservation to end the
outgoing guest's stay early, running checkout actions immediately.
Sets end_at to the outgoing record's check-out datetime when known,
so normal operation resumes automatically once the original
reservation period would have ended.
ZiB 3 mesiacov pred
rodič
commit
62a9f906a4

+ 29 - 2
custom_components/room_cal_ha/button.py

@@ -1,6 +1,6 @@
 from __future__ import annotations
 
-from datetime import timedelta
+from datetime import datetime, timedelta
 
 from homeassistant.components.button import ButtonEntity
 from homeassistant.config_entries import ConfigEntry
@@ -12,7 +12,7 @@ from homeassistant.util import dt as dt_util
 
 from .const import CONF_ROOM_NAME, DOMAIN, STORE_NEXT
 from .coordinator import RoomCalCoordinator
-from .models import OverrideData
+from .models import OverrideData, ReservationData
 
 
 async def async_setup_entry(
@@ -27,6 +27,7 @@ async def async_setup_entry(
     PromoteFromNextButton(coordinator, entry, device_name),
     ClearCurrentButton(coordinator, entry, device_name),
     TestNextGuestButton(coordinator, entry, device_name),
+    OverrideEmptyButton(coordinator, entry, device_name),
     GuestArrivedButton(coordinator, entry, device_name),
     GuestDepartedButton(coordinator, entry, device_name),
   ])
@@ -126,6 +127,32 @@ class TestNextGuestButton(CoordinatorEntity[RoomCalCoordinator], ButtonEntity):
     await self.coordinator.async_start_override(OverrideData(current=override_current))
 
 
+class OverrideEmptyButton(CoordinatorEntity[RoomCalCoordinator], ButtonEntity):
+  _attr_has_entity_name = True
+  _attr_name = "Override Empty"
+  _attr_icon = "mdi:calendar-remove"
+
+  def __init__(
+    self,
+    coordinator: RoomCalCoordinator,
+    entry: ConfigEntry,
+    device_name: str,
+  ) -> None:
+    super().__init__(coordinator)
+    self._attr_unique_id = f"{entry.entry_id}_override_empty"
+    self._attr_device_info = DeviceInfo(
+      identifiers={(DOMAIN, f"{entry.entry_id}_room_state")},
+      name=device_name,
+    )
+
+  async def async_press(self) -> None:
+    outgoing = self.coordinator.data.current if self.coordinator.data else None
+    end_at: datetime | None = None
+    if outgoing is not None and outgoing.check_out_date is not None and outgoing.check_out_time is not None:
+      end_at = datetime.combine(outgoing.check_out_date, outgoing.check_out_time)
+    await self.coordinator.async_start_override(OverrideData(current=ReservationData()), end_at=end_at)
+
+
 class GuestArrivedButton(ButtonEntity):
   _attr_has_entity_name = True
   _attr_name = "Guest Arrived"

+ 59 - 0
tests/test_buttons.py

@@ -11,6 +11,7 @@ import pytest
 from custom_components.room_cal_ha.button import (
   ClearCurrentButton,
   ForceRefreshButton,
+  OverrideEmptyButton,
   PromoteFromNextButton,
   TestNextGuestButton,
 )
@@ -143,3 +144,61 @@ async def test_test_next_guest_no_next_data_does_nothing():
   with patch("custom_components.room_cal_ha.button.dt_util.now", return_value=_FROZEN_NOW):
     await btn.async_press()
   btn.coordinator.async_start_override.assert_not_awaited()
+
+
+# ---------------------------------------------------------------------------
+# OverrideEmptyButton
+# ---------------------------------------------------------------------------
+
+_OUTGOING = ReservationData(
+  guest_name="Alice",
+  check_in_date=date(2026, 7, 1),
+  check_in_time=time(14, 0),
+  check_out_date=date(2026, 7, 5),
+  check_out_time=time(12, 0),
+)
+
+
+def _override_empty_button(current=_OUTGOING):
+  btn = object.__new__(OverrideEmptyButton)
+  btn.coordinator = MagicMock()
+  btn.coordinator.data = MagicMock(current=current) if current is not None else None
+  btn.coordinator.async_start_override = AsyncMock()
+  return btn
+
+
+async def test_override_empty_calls_start_override():
+  btn = _override_empty_button()
+  await btn.async_press()
+  btn.coordinator.async_start_override.assert_awaited_once()
+
+
+async def test_override_empty_passes_empty_reservation_data():
+  btn = _override_empty_button()
+  await btn.async_press()
+  override: OverrideData = btn.coordinator.async_start_override.call_args[0][0]
+  assert isinstance(override, OverrideData)
+  assert override.current == ReservationData()
+  assert override.next is None
+
+
+async def test_override_empty_end_at_matches_outgoing_checkout():
+  btn = _override_empty_button()
+  await btn.async_press()
+  kwargs = btn.coordinator.async_start_override.call_args[1]
+  assert kwargs["end_at"] == datetime(2026, 7, 5, 12, 0, 0)
+
+
+async def test_override_empty_no_end_at_when_outgoing_has_no_checkout():
+  btn = _override_empty_button(current=ReservationData(guest_name="Alice"))
+  await btn.async_press()
+  kwargs = btn.coordinator.async_start_override.call_args[1]
+  assert kwargs["end_at"] is None
+
+
+async def test_override_empty_no_end_at_when_no_current_data():
+  btn = _override_empty_button(current=None)
+  btn.coordinator.data = None
+  await btn.async_press()
+  kwargs = btn.coordinator.async_start_override.call_args[1]
+  assert kwargs["end_at"] is None