245 lines
9.4 KiB
Python
245 lines
9.4 KiB
Python
|
from datetime import datetime, timedelta
|
||
|
|
||
|
from flask import Flask, request
|
||
|
|
||
|
from .data import DataFile, DateTimeSpan, MinuteSpan
|
||
|
|
||
|
|
||
|
DATA = DataFile("data.json")
|
||
|
|
||
|
app = Flask(__name__)
|
||
|
|
||
|
|
||
|
def get_action_at(point_in_time, schedules, overrides):
|
||
|
for override in overrides:
|
||
|
if override.start <= point_in_time < override.end:
|
||
|
return override.action
|
||
|
for span in schedules[point_in_time.weekday()]:
|
||
|
span = DateTimeSpan.from_minutespan(point_in_time, span)
|
||
|
if span.start <= point_in_time < span.end:
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
|
||
|
def get_next_action_change(point_in_time, action, schedules, overrides):
|
||
|
skip_schedule = False
|
||
|
if not any(schedules.values()):
|
||
|
# No daily schedule is set
|
||
|
overrides = [o for o in overrides if o.action]
|
||
|
if not overrides:
|
||
|
# No allow overrides exist => denied indefinitely
|
||
|
return None
|
||
|
skip_schedule = True
|
||
|
elif all(o == [MinuteSpan(0, 24 * 60)] for o in schedules.values()):
|
||
|
# Daily schedule allowed 24/7
|
||
|
overrides = [o for o in overrides if not o.action]
|
||
|
if not overrides:
|
||
|
# No deny overrides exist => allowed indefinitely
|
||
|
return None
|
||
|
skip_schedule = True
|
||
|
if skip_schedule:
|
||
|
# Schedule is either empty or allows 24/7, therefore irrelevant
|
||
|
# and only overrides are considered.
|
||
|
for override_span in overrides:
|
||
|
if override_span.start > point_in_time:
|
||
|
# Nearest override didn't start yet
|
||
|
return override_span.start
|
||
|
if override_span.start <= point_in_time < override_span.end:
|
||
|
# We're in override
|
||
|
return override_span.end
|
||
|
# No more overrides => allowed / denied indefinitely
|
||
|
return None
|
||
|
|
||
|
while True:
|
||
|
new_point_in_time = None
|
||
|
for override_span in overrides:
|
||
|
if override_span.start <= point_in_time < override_span.end:
|
||
|
# We're in override
|
||
|
new_point_in_time = override_span.end
|
||
|
if new_point_in_time is None:
|
||
|
dow = point_in_time.weekday()
|
||
|
for span in schedules[dow]:
|
||
|
span = DateTimeSpan.from_minutespan(point_in_time, span)
|
||
|
if span.end < point_in_time:
|
||
|
# Skip already elapsed spans
|
||
|
continue
|
||
|
if span.start <= point_in_time < span.end:
|
||
|
# We're in span allowed by schedule, get the span end
|
||
|
new_point_in_time = span.end
|
||
|
# Check if any upcoming override comes sooner that the span end
|
||
|
for override_span in overrides:
|
||
|
if override_span.start >= point_in_time and override_span.start < span.end:
|
||
|
new_point_in_time = override_span.start
|
||
|
break
|
||
|
break
|
||
|
if new_point_in_time is None:
|
||
|
# We're in span denied by schedule, get next span start
|
||
|
days = 0
|
||
|
while True:
|
||
|
for span in schedules[(dow + days) % 7]:
|
||
|
span = DateTimeSpan.from_minutespan(point_in_time + timedelta(days=days), span)
|
||
|
if span.start > point_in_time:
|
||
|
new_point_in_time = span.start
|
||
|
# Check if any upcoming override comes sooner that the span start
|
||
|
for override_span in overrides:
|
||
|
if (
|
||
|
override_span.start >= point_in_time
|
||
|
and override_span.start < span.start
|
||
|
):
|
||
|
new_point_in_time = override_span.start
|
||
|
break
|
||
|
break
|
||
|
if new_point_in_time:
|
||
|
break
|
||
|
days += 1
|
||
|
if action != get_action_at(new_point_in_time, schedules, overrides):
|
||
|
return new_point_in_time
|
||
|
point_in_time = new_point_in_time
|
||
|
|
||
|
|
||
|
def update_status(status, schedules, overrides):
|
||
|
now = datetime.now()
|
||
|
status.action = get_action_at(now, schedules, overrides)
|
||
|
status.until = get_next_action_change(now, status.action, schedules, overrides)
|
||
|
status.then_until = (
|
||
|
get_next_action_change(status.until, not status.action, schedules, overrides)
|
||
|
if status.until
|
||
|
else None
|
||
|
)
|
||
|
|
||
|
|
||
|
def update_schedule(schedule, span, allow):
|
||
|
if allow:
|
||
|
schedule.append(span)
|
||
|
schedule.sort()
|
||
|
i = len(schedule) - 1
|
||
|
while i:
|
||
|
if schedule[i - 1].end >= schedule[i].start:
|
||
|
# spans overlap -> merge together
|
||
|
schedule[i - 1].start = min(schedule[i - 1].start, schedule[i].start)
|
||
|
schedule[i - 1].end = max(schedule[i - 1].end, schedule[i].end)
|
||
|
del schedule[i]
|
||
|
i -= 1
|
||
|
else:
|
||
|
i = len(schedule) - 1
|
||
|
while i >= 0:
|
||
|
if span.start <= schedule[i].start and span.end >= schedule[i].end:
|
||
|
# deny period larger than span -> remove span
|
||
|
del schedule[i]
|
||
|
elif span.start > schedule[i].start and span.end < schedule[i].end:
|
||
|
# deny period shorter than span -> split span into two
|
||
|
schedule.append(MinuteSpan(span.end, schedule[i].end))
|
||
|
schedule[i].end = span.start
|
||
|
elif span.start <= schedule[i].start < span.end:
|
||
|
# span starts within deny period -> move start
|
||
|
schedule[i].start = span.end
|
||
|
elif span.start < schedule[i].end <= span.end:
|
||
|
# span ends within deny period -> move end
|
||
|
schedule[i].end = span.start
|
||
|
i -= 1
|
||
|
schedule.sort()
|
||
|
|
||
|
|
||
|
def update_overrides(overrides, span):
|
||
|
now = datetime.now()
|
||
|
if span.end <= now:
|
||
|
# The span is in the past, nothing to do
|
||
|
return
|
||
|
i = len(overrides) - 1
|
||
|
while i >= 0:
|
||
|
if span.start <= overrides[i].start and span.end >= overrides[i].end:
|
||
|
# new override period larger than old override -> remove old override
|
||
|
del overrides[i]
|
||
|
elif span.start > overrides[i].start and span.end < overrides[i].end:
|
||
|
if span.start > now:
|
||
|
# new override period shorter than old override -> split old override into two
|
||
|
overrides.append(DateTimeSpan(span.end, overrides[i].end, overrides[i].action))
|
||
|
overrides[i].end = span.start
|
||
|
else:
|
||
|
# new override period would split the override, but the first part
|
||
|
# has already elapsed -> move old override start
|
||
|
overrides[i].start = span.end
|
||
|
elif span.start <= overrides[i].start < span.end:
|
||
|
# new override starts within old overrides period -> move old override start
|
||
|
overrides[i].start = span.end
|
||
|
elif span.start < overrides[i].end <= span.end:
|
||
|
# new override ends within old overrides period -> move old override end
|
||
|
overrides[i].end = span.start
|
||
|
i -= 1
|
||
|
overrides.append(span)
|
||
|
overrides.sort()
|
||
|
i = len(overrides) - 1
|
||
|
while i:
|
||
|
if (
|
||
|
overrides[i - 1].end >= overrides[i].start
|
||
|
and overrides[i - 1].action == overrides[i].action
|
||
|
):
|
||
|
# overrides of the same type overlap -> merge together
|
||
|
overrides[i - 1].start = min(overrides[i - 1].start, overrides[i].start)
|
||
|
overrides[i - 1].end = max(overrides[i - 1].end, overrides[i].end)
|
||
|
del overrides[i]
|
||
|
i -= 1
|
||
|
|
||
|
|
||
|
@app.get("/data")
|
||
|
def get_data():
|
||
|
with DATA as data:
|
||
|
if data.status.until and datetime.now() >= data.status.until:
|
||
|
update_status(data.status, data.schedules, data.overrides)
|
||
|
data.save()
|
||
|
return data.to_dict()
|
||
|
|
||
|
|
||
|
@app.post("/override")
|
||
|
def post_override():
|
||
|
span = DateTimeSpan.from_args(**request.json)
|
||
|
with DATA as data:
|
||
|
update_overrides(data.overrides, span)
|
||
|
update_status(data.status, data.schedules, data.overrides)
|
||
|
data.save()
|
||
|
return data.to_dict()
|
||
|
|
||
|
|
||
|
@app.post("/delete-override")
|
||
|
def delete_override():
|
||
|
span = DateTimeSpan.from_args(**request.json)
|
||
|
with DATA as data:
|
||
|
try:
|
||
|
data.overrides.remove(span)
|
||
|
update_status(data.status, data.schedules, data.overrides)
|
||
|
data.save()
|
||
|
except ValueError:
|
||
|
pass
|
||
|
return data.to_dict()
|
||
|
|
||
|
|
||
|
@app.post("/schedule")
|
||
|
def post_schedule():
|
||
|
schedule = request.json
|
||
|
span = MinuteSpan.from_args(**schedule)
|
||
|
with DATA as data:
|
||
|
for day in schedule["days"]:
|
||
|
update_schedule(data.schedules[day], span, schedule["action"])
|
||
|
update_status(data.status, data.schedules, data.overrides)
|
||
|
data.save()
|
||
|
return data.to_dict()
|
||
|
|
||
|
|
||
|
@app.get("/fast-action/<int:minutes>")
|
||
|
def fast_action(minutes):
|
||
|
with DATA as data:
|
||
|
now = datetime.now().replace(second=0, microsecond=0)
|
||
|
if minutes:
|
||
|
start = now
|
||
|
action = get_action_at(now, data.schedules, data.overrides)
|
||
|
if action:
|
||
|
start = get_next_action_change(now, action, data.schedules, data.overrides)
|
||
|
end = start + timedelta(minutes=minutes)
|
||
|
span = DateTimeSpan(start, end, True)
|
||
|
else:
|
||
|
span = DateTimeSpan(now, now + timedelta(minutes=5), False)
|
||
|
update_overrides(data.overrides, span)
|
||
|
update_status(data.status, data.schedules, data.overrides)
|
||
|
data.save()
|
||
|
return data.to_dict()
|