Răsfoiți Sursa

Add a dedicated Active device mirroring Current's full sensor set

Only Current/Next had guest_name/check-in/wifi/door_code sensors;
Active only had switches/buttons plus a Last Updated timestamp. That
left customer-facing consumers (signage, door displays) with nowhere
to read the live/effective record during an override -- they'd have
had to read Current, which is now a pure backend mirror and goes
blind to overrides.

Adds an Active device with the same 8-sensor shape as Current/Next,
reading coordinator.data.active. Moves Arrived/Departed there too
(they're live-guest-only concepts) instead of Room State, and folds
the one-off ActiveLastUpdatedSensor into the shared LastUpdatedSensor
class via a third slot branch.
ZiB 3 luni în urmă
părinte
comite
c65c899ef8

+ 7 - 2
custom_components/room_cal_ha/binary_sensor.py

@@ -17,6 +17,7 @@ from .const import (
   CONF_SYNC_MODE,
   DEFAULT_REFRESH_INTERVAL,
   DOMAIN,
+  SLOT_ACTIVE,
   SYNC_MODE_MANUAL,
 )
 from .coordinator import CoordinatorData, RoomCalCoordinator
@@ -33,10 +34,14 @@ async def async_setup_entry(
     identifiers={(DOMAIN, f"{entry.entry_id}_room_state")},
     name=f"{room_name} Room State",
   )
+  active_device = DeviceInfo(
+    identifiers={(DOMAIN, f"{entry.entry_id}_{SLOT_ACTIVE}")},
+    name=f"{room_name} Active",
+  )
   async_add_entities([
     StaleSensor(coordinator, entry, room_state_device),
-    ArrivedSensor(coordinator, entry, room_state_device),
-    DepartedSensor(coordinator, entry, room_state_device),
+    ArrivedSensor(coordinator, entry, active_device),
+    DepartedSensor(coordinator, entry, active_device),
   ])
 
 

+ 5 - 0
custom_components/room_cal_ha/const.py

@@ -52,3 +52,8 @@ STORE_NEXT = "next"
 STORE_ARRIVED = "active_arrived"
 STORE_DEPARTED = "active_departed"
 
+# Display slot identifier for the live/maskable record — not a store key (Active is
+# never independently persisted; see coordinator.py). Used only to group sensor/device
+# setup alongside STORE_CURRENT/STORE_NEXT.
+SLOT_ACTIVE = "active"
+

+ 15 - 41
custom_components/room_cal_ha/sensor.py

@@ -17,6 +17,7 @@ from .const import (
   CONF_ROOM_NAME,
   CONF_SYNC_MODE,
   DOMAIN,
+  SLOT_ACTIVE,
   STORE_CURRENT,
   STORE_NEXT,
   SYNC_MODE_API,
@@ -43,13 +44,17 @@ async def async_setup_entry(
 
   entities: list[SensorEntity] = [
     SyncSourceSensor(entry, room_name, room_state_device),
-    ActiveLastUpdatedSensor(coordinator, entry, room_state_device),
   ]
 
   if entry.options.get(CONF_SYNC_MODE) == SYNC_MODE_API:
     entities.append(ApiHealthSensor(coordinator, entry, room_state_device))
 
+  # Active is listed first: it's the live/effective record (overridable) that in-room,
+  # customer-facing consumers (signage, door displays) should read. Current/Next are
+  # pure backend mirrors — never masked by an override — for employee-facing views that
+  # want to see the raw upcoming feed regardless of any in-progress override.
   for slot, device_name in (
+    (SLOT_ACTIVE, f"{room_name} Active"),
     (STORE_CURRENT, f"{room_name} Current"),
     (STORE_NEXT, f"{room_name} Next"),
   ):
@@ -123,6 +128,8 @@ class _ReservationSensor(CoordinatorEntity[RoomCalCoordinator], SensorEntity):
 
   def _reservation(self) -> ReservationData:
     data: CoordinatorData = self.coordinator.data
+    if self._slot == SLOT_ACTIVE:
+      return data.active
     return data.current if self._slot == STORE_CURRENT else data.next
 
 
@@ -264,53 +271,20 @@ class LastUpdatedSensor(CoordinatorEntity[RoomCalCoordinator], SensorEntity):
   @property
   def native_value(self) -> datetime | None:
     data: CoordinatorData = self.coordinator.data
-    return (
-      data.current_last_updated
-      if self._slot == STORE_CURRENT
-      else data.next_last_updated
-    )
+    if self._slot == SLOT_ACTIVE:
+      return data.active_last_updated
+    return data.current_last_updated if self._slot == STORE_CURRENT else data.next_last_updated
 
   @property
   def extra_state_attributes(self) -> dict:
     data: CoordinatorData = self.coordinator.data
-    refresh = (
-      data.current_last_refresh
-      if self._slot == STORE_CURRENT
-      else data.next_last_refresh
-    )
+    if self._slot == SLOT_ACTIVE:
+      refresh = data.active_last_refresh
+    else:
+      refresh = data.current_last_refresh if self._slot == STORE_CURRENT else data.next_last_refresh
     return {"last_refresh": refresh}
 
 
-class ActiveLastUpdatedSensor(CoordinatorEntity[RoomCalCoordinator], SensorEntity):
-  """Last time the live/active record changed — distinct from the Current device's
-  LastUpdatedSensor, which only tracks the backend-mirror feed and never reflects
-  override/patch activity."""
-
-  _attr_has_entity_name = True
-  _attr_translation_key = "active_last_updated"
-  _attr_device_class = SensorDeviceClass.TIMESTAMP
-  _attr_icon = "mdi:clock-edit-outline"
-  _attr_entity_category = EntityCategory.DIAGNOSTIC
-
-  def __init__(
-    self,
-    coordinator: RoomCalCoordinator,
-    entry: ConfigEntry,
-    device_info: DeviceInfo,
-  ) -> None:
-    super().__init__(coordinator)
-    self._attr_unique_id = f"{entry.entry_id}_active_last_updated"
-    self._attr_device_info = device_info
-
-  @property
-  def native_value(self) -> datetime | None:
-    return self.coordinator.data.active_last_updated
-
-  @property
-  def extra_state_attributes(self) -> dict:
-    return {"last_refresh": self.coordinator.data.active_last_refresh}
-
-
 class ApiHealthSensor(CoordinatorEntity[RoomCalCoordinator], SensorEntity):
   _attr_has_entity_name = True
   _attr_translation_key = "api_health"

+ 0 - 1
custom_components/room_cal_ha/strings.json

@@ -104,7 +104,6 @@
       "wifi_key": { "name": "WiFi Key" },
       "door_code": { "name": "Door Code" },
       "last_updated": { "name": "Last Updated" },
-      "active_last_updated": { "name": "Active Last Updated" },
       "api_health": { "name": "API Health" }
     },
     "binary_sensor": {

+ 0 - 1
custom_components/room_cal_ha/translations/en.json

@@ -104,7 +104,6 @@
       "wifi_key": { "name": "WiFi Key" },
       "door_code": { "name": "Door Code" },
       "last_updated": { "name": "Last Updated" },
-      "active_last_updated": { "name": "Active Last Updated" },
       "api_health": { "name": "API Health" }
     },
     "binary_sensor": {

+ 32 - 2
tests/test_sensors.py

@@ -19,7 +19,7 @@ from custom_components.room_cal_ha.sensor import (
   ReservationTimeSensor,
   WiFiKeySensor,
 )
-from custom_components.room_cal_ha.const import STORE_CURRENT, STORE_NEXT
+from custom_components.room_cal_ha.const import SLOT_ACTIVE, STORE_CURRENT, STORE_NEXT
 
 
 # ---------------------------------------------------------------------------
@@ -27,23 +27,28 @@ from custom_components.room_cal_ha.const import STORE_CURRENT, STORE_NEXT
 # ---------------------------------------------------------------------------
 
 def _coordinator(
+  active=None,
   current=None,
   nxt=None,
   current_ts=None,
   next_ts=None,
   current_updated=None,
   next_updated=None,
+  active_ts=None,
+  active_updated=None,
   api_status=None,
 ):
   coord = MagicMock()
   coord.data = CoordinatorData(
-    active=current or ReservationData(),
+    active=active if active is not None else (current or ReservationData()),
     current=current or ReservationData(),
     next=nxt or ReservationData(),
     current_last_refresh=current_ts,
     next_last_refresh=next_ts,
     current_last_updated=current_updated,
     next_last_updated=next_updated,
+    active_last_refresh=active_ts,
+    active_last_updated=active_updated,
     api_status=api_status,
   )
   return coord
@@ -96,6 +101,15 @@ def test_str_sensor_none_when_field_is_none():
   assert s.native_value is None
 
 
+def test_str_sensor_reads_active_slot():
+  """Active sensors read the live/effective record — distinct from Current, which
+  never reflects an in-progress override."""
+  active = ReservationData(guest_name="Override Guest")
+  coord = _coordinator(active=active, current=ReservationData(guest_name="Backend Guest"))
+  s = _sensor(ReservationStrSensor, coord, slot=SLOT_ACTIVE, field="guest_name")
+  assert s.native_value == "Override Guest"
+
+
 # ---------------------------------------------------------------------------
 # GuestNameSensor
 # ---------------------------------------------------------------------------
@@ -286,6 +300,22 @@ def test_last_updated_sensor_last_refresh_attribute_none_when_not_yet_fetched():
   assert s.extra_state_attributes["last_refresh"] is None
 
 
+def test_last_updated_sensor_active():
+  ts = datetime(2026, 6, 22, 11, 0, tzinfo=timezone.utc)
+  coord = _coordinator(active_updated=ts)
+  s = _sensor(LastUpdatedSensor, coord)
+  s._slot = SLOT_ACTIVE
+  assert s.native_value == ts
+
+
+def test_last_updated_sensor_active_exposes_active_last_refresh():
+  refresh_ts = datetime(2026, 6, 22, 9, 30, tzinfo=timezone.utc)
+  coord = _coordinator(active_ts=refresh_ts)
+  s = _sensor(LastUpdatedSensor, coord)
+  s._slot = SLOT_ACTIVE
+  assert s.extra_state_attributes["last_refresh"] == refresh_ts
+
+
 # ---------------------------------------------------------------------------
 # ApiHealthSensor
 # ---------------------------------------------------------------------------