|
|
@@ -13,6 +13,7 @@ from homeassistant.util import dt as dt_util
|
|
|
|
|
|
from .backends import BackendAuthError, BackendConnectError, CalDavBackend, RoomCalApiBackend
|
|
|
from .const import (
|
|
|
+ CONF_GUEST_SCRIPT,
|
|
|
CONF_REFRESH_INTERVAL,
|
|
|
CONF_RESERVATION_DEFAULTS,
|
|
|
CONF_SYNC_MODE,
|
|
|
@@ -26,7 +27,7 @@ from .const import (
|
|
|
SYNC_MODE_CALDAV,
|
|
|
)
|
|
|
from .models import ApiStatusData, OverrideData, ReservationData, is_window_active, reservation_diff
|
|
|
-from dataclasses import fields as dataclass_fields
|
|
|
+from dataclasses import fields as dataclass_fields, replace as dataclass_replace
|
|
|
from .store import ReservationStore
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
@@ -42,6 +43,8 @@ class CoordinatorData:
|
|
|
next_last_updated: datetime | None = None
|
|
|
override_active: bool = False
|
|
|
api_status: ApiStatusData | None = None
|
|
|
+ current_arrived: bool = False
|
|
|
+ current_departed: bool = False
|
|
|
|
|
|
|
|
|
class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
@@ -63,11 +66,15 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
self._current_last_updated: datetime | None = None
|
|
|
self._next_last_updated: datetime | None = None
|
|
|
self._override: OverrideData | None = None
|
|
|
+ self._current_arrived: bool = False
|
|
|
+ self._current_departed: bool = False
|
|
|
|
|
|
async def async_setup(self) -> None:
|
|
|
await self.store.async_load()
|
|
|
self._current_last_updated = self.store.get_last_updated(STORE_CURRENT)
|
|
|
self._next_last_updated = self.store.get_last_updated(STORE_NEXT)
|
|
|
+ self._current_arrived = self.store.get_arrived()
|
|
|
+ self._current_departed = self.store.get_departed()
|
|
|
|
|
|
def inject_room_state_switch(self, switch: Any) -> None:
|
|
|
self._room_state_switch = switch
|
|
|
@@ -141,6 +148,7 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
old_current = self.store.get(STORE_CURRENT)
|
|
|
await self.store.async_set(STORE_CURRENT, backend_current)
|
|
|
await self.store.async_set(STORE_NEXT, backend_next)
|
|
|
+ await self._async_maybe_reset_arrival_flags(old_current, backend_current)
|
|
|
self._current_last_refresh = now
|
|
|
self._next_last_refresh = now
|
|
|
if self._override is None:
|
|
|
@@ -169,6 +177,8 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
next_last_updated=self._next_last_updated,
|
|
|
override_active=True,
|
|
|
api_status=api_status,
|
|
|
+ current_arrived=self._current_arrived,
|
|
|
+ current_departed=self._current_departed,
|
|
|
)
|
|
|
|
|
|
await self._async_note_displayed_change(store_current, store_next, now)
|
|
|
@@ -182,6 +192,8 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
current_last_updated=self._current_last_updated,
|
|
|
next_last_updated=self._next_last_updated,
|
|
|
api_status=api_status,
|
|
|
+ current_arrived=self._current_arrived,
|
|
|
+ current_departed=self._current_departed,
|
|
|
)
|
|
|
|
|
|
def _reschedule_mode_sync(self, current: ReservationData, nxt: ReservationData) -> None:
|
|
|
@@ -352,18 +364,22 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
_startup=True uses direct state mutation on the room state switch instead of
|
|
|
async_turn_on/off, because HA state writes are deferred until commit_state().
|
|
|
"""
|
|
|
- current = self.store.get(STORE_CURRENT) or self._empty_reservation()
|
|
|
+ old_current = self.store.get(STORE_CURRENT)
|
|
|
+ current = old_current or self._empty_reservation()
|
|
|
nxt = self.store.get(STORE_NEXT) or self._empty_reservation()
|
|
|
|
|
|
if self._is_mode_sync_on():
|
|
|
if self._reservation_window_state(nxt) in ("active", "elapsed"):
|
|
|
await self.store.async_set(STORE_CURRENT, nxt)
|
|
|
+ await self._async_maybe_reset_arrival_flags(old_current, nxt)
|
|
|
self._current_last_refresh = dt_util.utcnow()
|
|
|
current = nxt
|
|
|
+ old_current = nxt
|
|
|
|
|
|
state = self._reservation_window_state(current)
|
|
|
if state == "elapsed":
|
|
|
await self.store.async_set(STORE_CURRENT, None)
|
|
|
+ await self._async_maybe_reset_arrival_flags(old_current, None)
|
|
|
self._current_last_refresh = dt_util.utcnow()
|
|
|
current = self._empty_reservation()
|
|
|
|
|
|
@@ -382,6 +398,8 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
current_last_updated=self._current_last_updated,
|
|
|
next_last_updated=self._next_last_updated,
|
|
|
api_status=self.data.api_status if self.data else None,
|
|
|
+ current_arrived=self._current_arrived,
|
|
|
+ current_departed=self._current_departed,
|
|
|
))
|
|
|
|
|
|
async def async_post_setup_reconcile(self) -> None:
|
|
|
@@ -427,6 +445,7 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
async def _async_do_check_out(self) -> None:
|
|
|
departing = self.store.get(STORE_CURRENT) or self._empty_reservation()
|
|
|
await self.store.async_set(STORE_CURRENT, None)
|
|
|
+ await self._async_maybe_reset_arrival_flags(departing, None)
|
|
|
now = dt_util.utcnow()
|
|
|
self._current_last_refresh = now
|
|
|
nxt = self.store.get(STORE_NEXT) or self._empty_reservation()
|
|
|
@@ -440,6 +459,8 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
current_last_updated=self._current_last_updated,
|
|
|
next_last_updated=self._next_last_updated,
|
|
|
api_status=self.data.api_status if self.data else None,
|
|
|
+ current_arrived=self._current_arrived,
|
|
|
+ current_departed=self._current_departed,
|
|
|
))
|
|
|
await self._async_run_check_out_actions(departing)
|
|
|
self._reschedule_mode_sync(empty, nxt)
|
|
|
@@ -448,6 +469,7 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
"""Clear the Current slot in the store and notify listeners."""
|
|
|
departing = self.store.get(STORE_CURRENT)
|
|
|
await self.store.async_set(STORE_CURRENT, None)
|
|
|
+ await self._async_maybe_reset_arrival_flags(departing, None)
|
|
|
now = dt_util.utcnow()
|
|
|
self._current_last_refresh = now
|
|
|
nxt = self.store.get(STORE_NEXT) or self._empty_reservation()
|
|
|
@@ -461,6 +483,8 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
current_last_updated=self._current_last_updated,
|
|
|
next_last_updated=self._next_last_updated,
|
|
|
api_status=self.data.api_status if self.data else None,
|
|
|
+ current_arrived=self._current_arrived,
|
|
|
+ current_departed=self._current_departed,
|
|
|
))
|
|
|
if departing is not None:
|
|
|
await self._async_run_check_out_actions(departing)
|
|
|
@@ -471,6 +495,7 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
old_current = self.store.get(STORE_CURRENT)
|
|
|
next_data = self.store.get(STORE_NEXT) or self._empty_reservation()
|
|
|
await self.store.async_set(STORE_CURRENT, next_data)
|
|
|
+ await self._async_maybe_reset_arrival_flags(old_current, next_data)
|
|
|
now = dt_util.utcnow()
|
|
|
self._current_last_refresh = now
|
|
|
new_next = self.store.get(STORE_NEXT) or self._empty_reservation()
|
|
|
@@ -483,6 +508,8 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
current_last_updated=self._current_last_updated,
|
|
|
next_last_updated=self._next_last_updated,
|
|
|
api_status=self.data.api_status if self.data else None,
|
|
|
+ current_arrived=self._current_arrived,
|
|
|
+ current_departed=self._current_departed,
|
|
|
))
|
|
|
self._reschedule_mode_sync(next_data, new_next)
|
|
|
if self._reservation_window_state(next_data) == "active":
|
|
|
@@ -508,6 +535,8 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
next_last_updated=self._next_last_updated,
|
|
|
override_active=True,
|
|
|
api_status=self.data.api_status if self.data else None,
|
|
|
+ current_arrived=self._current_arrived,
|
|
|
+ current_departed=self._current_departed,
|
|
|
))
|
|
|
await self._async_run_check_in_actions(override.current)
|
|
|
|
|
|
@@ -563,6 +592,8 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
next_last_updated=self._next_last_updated,
|
|
|
override_active=True,
|
|
|
api_status=self.data.api_status if self.data else None,
|
|
|
+ current_arrived=self._current_arrived,
|
|
|
+ current_departed=self._current_departed,
|
|
|
))
|
|
|
return
|
|
|
|
|
|
@@ -590,6 +621,8 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
next_last_updated=self._next_last_updated,
|
|
|
override_active=True,
|
|
|
api_status=self.data.api_status if self.data else None,
|
|
|
+ current_arrived=self._current_arrived,
|
|
|
+ current_departed=self._current_departed,
|
|
|
))
|
|
|
else:
|
|
|
store_current = self.store.get(STORE_CURRENT) or self._empty_reservation()
|
|
|
@@ -603,6 +636,8 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
current_last_updated=self._current_last_updated,
|
|
|
next_last_updated=self._next_last_updated,
|
|
|
api_status=self.data.api_status if self.data else None,
|
|
|
+ current_arrived=self._current_arrived,
|
|
|
+ current_departed=self._current_departed,
|
|
|
))
|
|
|
self._reschedule_mode_sync(store_current, store_next)
|
|
|
if target == STORE_CURRENT:
|
|
|
@@ -659,6 +694,52 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
|
|
departing=diff.departing,
|
|
|
)
|
|
|
|
|
|
+ @staticmethod
|
|
|
+ def _arrival_identity(res: ReservationData | None) -> tuple:
|
|
|
+ if res is None:
|
|
|
+ return (None, None)
|
|
|
+ return (res.check_in_date, res.check_in_time)
|
|
|
+
|
|
|
+ async def _async_maybe_reset_arrival_flags(
|
|
|
+ self,
|
|
|
+ old: ReservationData | None,
|
|
|
+ new: ReservationData | None,
|
|
|
+ ) -> None:
|
|
|
+ if self._arrival_identity(old) != self._arrival_identity(new):
|
|
|
+ self._current_arrived = False
|
|
|
+ self._current_departed = False
|
|
|
+ await self.store.async_set_arrived(False)
|
|
|
+ await self.store.async_set_departed(False)
|
|
|
+
|
|
|
+ async def async_set_arrived(self, arrived: bool) -> None:
|
|
|
+ self._current_arrived = arrived
|
|
|
+ await self.store.async_set_arrived(arrived)
|
|
|
+ if arrived:
|
|
|
+ await self._async_run_guest_script("arrived")
|
|
|
+ if self.data is not None:
|
|
|
+ self.async_set_updated_data(dataclass_replace(self.data, current_arrived=arrived))
|
|
|
+
|
|
|
+ async def async_set_departed(self, departed: bool) -> None:
|
|
|
+ self._current_departed = departed
|
|
|
+ await self.store.async_set_departed(departed)
|
|
|
+ if departed:
|
|
|
+ await self._async_run_guest_script("departed")
|
|
|
+ if self.data is not None:
|
|
|
+ self.async_set_updated_data(dataclass_replace(self.data, current_departed=departed))
|
|
|
+
|
|
|
+ async def _async_run_guest_script(self, event_type: str) -> None:
|
|
|
+ script_id = self.entry.options.get(CONF_GUEST_SCRIPT)
|
|
|
+ if not script_id:
|
|
|
+ return
|
|
|
+ try:
|
|
|
+ await self.hass.services.async_call(
|
|
|
+ "script", "turn_on",
|
|
|
+ {"entity_id": script_id, "variables": {"event_type": event_type}},
|
|
|
+ blocking=False,
|
|
|
+ )
|
|
|
+ except Exception as err:
|
|
|
+ _LOGGER.warning("Failed to run guest script '%s': %s", script_id, err)
|
|
|
+
|
|
|
def cancel_mode_sync(self) -> None:
|
|
|
for cancel in self._mode_sync_handles:
|
|
|
cancel()
|