feat: add support for one-off recording schedules with start and end datetime

This commit is contained in:
serversdwn
2026-02-10 07:08:03 +00:00
parent 3b29c4d645
commit 842e9d6f61
9 changed files with 508 additions and 9 deletions

View File

@@ -49,6 +49,8 @@ class RecurringScheduleService:
include_download: bool = True,
auto_increment_index: bool = True,
timezone: str = "America/New_York",
start_datetime: datetime = None,
end_datetime: datetime = None,
) -> RecurringSchedule:
"""
Create a new recurring schedule.
@@ -57,7 +59,7 @@ class RecurringScheduleService:
project_id: Project ID
location_id: Monitoring location ID
name: Schedule name
schedule_type: "weekly_calendar" or "simple_interval"
schedule_type: "weekly_calendar", "simple_interval", or "one_off"
device_type: "slm" or "seismograph"
unit_id: Specific unit (optional, can use assignment)
weekly_pattern: Dict of day patterns for weekly_calendar
@@ -66,6 +68,8 @@ class RecurringScheduleService:
include_download: Whether to download data on cycle
auto_increment_index: Whether to auto-increment store index before start
timezone: Timezone for schedule times
start_datetime: Start date+time in UTC (one_off only)
end_datetime: End date+time in UTC (one_off only)
Returns:
Created RecurringSchedule
@@ -85,6 +89,8 @@ class RecurringScheduleService:
auto_increment_index=auto_increment_index,
enabled=True,
timezone=timezone,
start_datetime=start_datetime,
end_datetime=end_datetime,
)
# Calculate next occurrence
@@ -213,6 +219,8 @@ class RecurringScheduleService:
actions = self._generate_weekly_calendar_actions(schedule, horizon_days)
elif schedule.schedule_type == "simple_interval":
actions = self._generate_interval_actions(schedule, horizon_days)
elif schedule.schedule_type == "one_off":
actions = self._generate_one_off_actions(schedule)
else:
logger.warning(f"Unknown schedule type: {schedule.schedule_type}")
return []
@@ -431,6 +439,77 @@ class RecurringScheduleService:
return actions
def _generate_one_off_actions(
self,
schedule: RecurringSchedule,
) -> List[ScheduledAction]:
"""
Generate start and stop actions for a one-off recording.
Unlike recurring types, this generates exactly one start and one stop action
using the schedule's start_datetime and end_datetime directly.
"""
if not schedule.start_datetime or not schedule.end_datetime:
logger.warning(f"One-off schedule {schedule.id} missing start/end datetime")
return []
actions = []
now_utc = datetime.utcnow()
unit_id = self._resolve_unit_id(schedule)
# Skip if end time has already passed
if schedule.end_datetime <= now_utc:
return []
# Check if actions already exist for this schedule
if self._action_exists(schedule.project_id, schedule.location_id, "start", schedule.start_datetime):
return []
# Create START action (only if start time hasn't passed)
if schedule.start_datetime > now_utc:
start_notes = json.dumps({
"schedule_name": schedule.name,
"schedule_id": schedule.id,
"schedule_type": "one_off",
"auto_increment_index": schedule.auto_increment_index,
})
start_action = ScheduledAction(
id=str(uuid.uuid4()),
project_id=schedule.project_id,
location_id=schedule.location_id,
unit_id=unit_id,
action_type="start",
device_type=schedule.device_type,
scheduled_time=schedule.start_datetime,
execution_status="pending",
notes=start_notes,
)
actions.append(start_action)
# Create STOP action
stop_notes = json.dumps({
"schedule_name": schedule.name,
"schedule_id": schedule.id,
"schedule_type": "one_off",
"include_download": schedule.include_download,
})
stop_action = ScheduledAction(
id=str(uuid.uuid4()),
project_id=schedule.project_id,
location_id=schedule.location_id,
unit_id=unit_id,
action_type="stop",
device_type=schedule.device_type,
scheduled_time=schedule.end_datetime,
execution_status="pending",
notes=stop_notes,
)
actions.append(stop_action)
return actions
def _calculate_next_occurrence(self, schedule: RecurringSchedule) -> Optional[datetime]:
"""Calculate when the next action should occur."""
if not schedule.enabled:
@@ -471,6 +550,13 @@ class RecurringScheduleService:
if cycle_utc > now_utc:
return cycle_utc
elif schedule.schedule_type == "one_off":
if schedule.start_datetime and schedule.start_datetime > now_utc:
return schedule.start_datetime
elif schedule.end_datetime and schedule.end_datetime > now_utc:
return schedule.end_datetime
return None
return None
def _resolve_unit_id(self, schedule: RecurringSchedule) -> Optional[str]: