parental-control/api/app.py

245 lines
9.4 KiB
Python
Raw Normal View History

2023-09-09 10:41:07 +02:00
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()