SafeEyes/safeeyes/core.py

319 lines
12 KiB
Python
Raw Permalink Normal View History

2017-10-07 15:10:31 +02:00
#!/usr/bin/env python
2016-10-15 06:11:27 +02:00
# Safe Eyes is a utility to remind you to take break frequently
# to protect your eyes from eye strain.
# Copyright (C) 2016 Gobinath
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
2017-10-07 15:10:31 +02:00
"""
SafeEyesCore provides the core functionalities of Safe Eyes.
"""
2016-10-15 06:11:27 +02:00
2017-10-07 15:10:31 +02:00
import datetime
import logging
import threading
import time
2017-02-06 03:09:04 +01:00
2020-03-18 13:33:11 +01:00
from safeeyes import utility
2017-10-07 15:10:31 +02:00
from safeeyes.model import Break
from safeeyes.model import BreakType
from safeeyes.model import BreakQueue
2017-10-07 15:10:31 +02:00
from safeeyes.model import EventHook
from safeeyes.model import State
2020-03-18 13:33:11 +01:00
class SafeEyesCore:
2017-10-07 15:10:31 +02:00
"""
Core of Safe Eyes runs the scheduler and notifies the breaks.
"""
def __init__(self, context):
"""
Create an instance of SafeEyesCore and initialize the variables.
"""
self.break_queue = None
2017-10-07 15:10:31 +02:00
self.postpone_duration = 0
self.default_postpone_duration = 0
2017-10-07 15:10:31 +02:00
self.pre_break_warning_time = 0
self.running = False
self.scheduled_next_break_timestamp = -1
self.scheduled_next_break_time = None
self.paused_time = -1
2017-10-07 15:10:31 +02:00
# This event is fired before <time-to-prepare> for a break
self.on_pre_break = EventHook()
# This event is fired just before the start of a break
2017-10-07 15:10:31 +02:00
self.on_start_break = EventHook()
# This event is fired at the start of a break
self.start_break = EventHook()
2017-10-07 15:10:31 +02:00
# This event is fired during every count down
self.on_count_down = EventHook()
# This event is fired at the end of a break
self.on_stop_break = EventHook()
# This event is fired when deciding the next break time
self.on_update_next_break = EventHook()
self.waiting_condition = threading.Condition()
self.lock = threading.Lock()
self.context = context
self.context['skipped'] = False
self.context['postponed'] = False
self.context['state'] = State.WAITING
def initialize(self, config):
"""
Initialize the internal properties from configuration
"""
logging.info("Initialize the core")
self.pre_break_warning_time = config.get('pre_break_warning_time')
self.break_queue = BreakQueue(config, self.context)
self.default_postpone_duration = config.get('postpone_duration') * 60 # Convert to seconds
self.postpone_duration = self.default_postpone_duration
2017-10-07 15:10:31 +02:00
def start(self, next_break_time=-1, reset_breaks=False):
2017-10-07 15:10:31 +02:00
"""
Start Safe Eyes is it is not running already.
"""
if self.break_queue.is_empty():
2017-10-07 15:10:31 +02:00
return
with self.lock:
if not self.running:
logging.info("Start Safe Eyes core")
if reset_breaks:
logging.info("Reset breaks to start from the beginning")
self.break_queue.reset()
2017-10-07 15:10:31 +02:00
self.running = True
self.scheduled_next_break_timestamp = int(next_break_time)
2020-03-18 13:33:11 +01:00
utility.start_thread(self.__scheduler_job)
2017-10-07 15:10:31 +02:00
def stop(self, is_resting=False):
2017-10-07 15:10:31 +02:00
"""
Stop Safe Eyes if it is running.
"""
with self.lock:
if not self.running:
return
logging.info("Stop Safe Eyes core")
self.paused_time = datetime.datetime.now().timestamp()
2017-10-07 15:10:31 +02:00
# Stop the break thread
self.waiting_condition.acquire()
self.running = False
if self.context['state'] != State.QUIT:
self.context['state'] = State.RESTING if (is_resting) else State.STOPPED
2017-10-07 15:10:31 +02:00
self.waiting_condition.notify_all()
self.waiting_condition.release()
def skip(self):
"""
User skipped the break using Skip button
"""
self.context['skipped'] = True
def postpone(self, duration=-1):
2017-10-07 15:10:31 +02:00
"""
User postponed the break using Postpone button
"""
if duration > 0:
self.postpone_duration = duration
else:
self.postpone_duration = self.default_postpone_duration
logging.debug("Postpone the break for %d seconds", self.postpone_duration)
2017-10-07 15:10:31 +02:00
self.context['postponed'] = True
2023-02-02 20:19:41 +01:00
def get_break_time(self, break_type = None):
"""
Returns the next break time
"""
break_obj = self.break_queue.get_break(break_type)
if not break_obj:
return False
time = self.scheduled_next_break_time + datetime.timedelta(minutes=break_obj.time - self.break_queue.get_break().time)
return time
def take_break(self, break_type = None):
2017-10-07 15:10:31 +02:00
"""
Calling this method stops the scheduler and show the next break screen
"""
if self.break_queue.is_empty():
2017-10-07 15:10:31 +02:00
return
if not self.context['state'] == State.WAITING:
return
utility.start_thread(self.__take_break, break_type=break_type)
2017-10-07 15:10:31 +02:00
def has_breaks(self, break_type = None):
2017-10-07 15:10:31 +02:00
"""
Check whether Safe Eyes has breaks or not. Use the break_type to check for either short or long break.
2017-10-07 15:10:31 +02:00
"""
return not self.break_queue.is_empty(break_type)
2017-10-07 15:10:31 +02:00
def __take_break(self, break_type = None):
2017-10-07 15:10:31 +02:00
"""
Show the next break screen
"""
logging.info('Take a break due to external request')
with self.lock:
if not self.running:
return
logging.info("Stop the scheduler")
# Stop the break thread
self.waiting_condition.acquire()
self.running = False
self.waiting_condition.notify_all()
self.waiting_condition.release()
time.sleep(1) # Wait for 1 sec to ensure the scheduler is dead
2017-10-07 15:10:31 +02:00
self.running = True
if break_type is not None and self.break_queue.get_break().type != break_type:
self.break_queue.next(break_type)
utility.execute_main_thread(self.__fire_start_break)
2017-10-07 15:10:31 +02:00
def __scheduler_job(self):
"""
Scheduler task to execute during every interval
"""
if not self.running:
return
current_time = datetime.datetime.now()
current_timestamp = current_time.timestamp()
2017-10-07 15:10:31 +02:00
if self.context['state'] == State.RESTING and self.paused_time > -1:
# Safe Eyes was resting
paused_duration = int(current_timestamp - self.paused_time)
self.paused_time = -1
if paused_duration > self.break_queue.get_break(BreakType.LONG_BREAK).duration:
logging.info('Skip next long break due to the pause %ds longer than break duration', paused_duration)
# Skip the next long break
self.break_queue.reset()
2017-10-07 15:10:31 +02:00
if self.context['postponed']:
# Previous break was postponed
logging.info('Prepare for postponed break')
time_to_wait = self.postpone_duration
self.context['postponed'] = False
elif current_timestamp < self.scheduled_next_break_timestamp:
# Non-standard break was set.
time_to_wait = round(self.scheduled_next_break_timestamp - current_timestamp)
self.scheduled_next_break_timestamp = -1
else:
# Use next break, convert to seconds
time_to_wait = self.break_queue.get_break().time * 60
self.scheduled_next_break_time = current_time + datetime.timedelta(seconds=time_to_wait)
self.context['state'] = State.WAITING
2020-03-18 13:33:11 +01:00
utility.execute_main_thread(self.__fire_on_update_next_break, self.scheduled_next_break_time)
2017-10-07 15:10:31 +02:00
# Wait for the pre break warning period
2017-10-17 19:07:46 +02:00
logging.info("Waiting for %d minutes until next break", (time_to_wait / 60))
self.__wait_for(time_to_wait)
2017-10-07 15:10:31 +02:00
logging.info("Pre-break waiting is over")
if not self.running:
return
2020-03-18 13:33:11 +01:00
utility.execute_main_thread(self.__fire_pre_break)
2017-10-07 15:10:31 +02:00
def __fire_on_update_next_break(self, next_break_time):
"""
Pass the next break information to the registered listeners.
"""
self.on_update_next_break.fire(self.break_queue.get_break(), next_break_time)
2017-10-07 15:10:31 +02:00
def __fire_pre_break(self):
"""
Show the notification and start the break after the notification.
"""
self.context['state'] = State.PRE_BREAK
if not self.on_pre_break.fire(self.break_queue.get_break()):
2017-10-07 15:10:31 +02:00
# Plugins wanted to ignore this break
self.__start_next_break()
return
2020-03-18 13:33:11 +01:00
utility.start_thread(self.__wait_until_prepare)
2017-10-07 15:10:31 +02:00
def __wait_until_prepare(self):
logging.info("Wait for %d seconds before the break", self.pre_break_warning_time)
# Wait for the pre break warning period
self.__wait_for(self.pre_break_warning_time)
if not self.running:
return
2020-03-18 13:33:11 +01:00
utility.execute_main_thread(self.__fire_start_break)
2017-10-07 15:10:31 +02:00
def __postpone_break(self):
self.__wait_for(self.postpone_duration)
2020-03-18 13:33:11 +01:00
utility.execute_main_thread(self.__fire_start_break)
def __fire_start_break(self):
break_obj = self.break_queue.get_break()
2017-10-07 15:10:31 +02:00
# Show the break screen
if not self.on_start_break.fire(break_obj):
# Plugins want to ignore this break
2017-10-07 15:10:31 +02:00
self.__start_next_break()
return
if self.context['postponed']:
# Plugins want to postpone this break
self.context['postponed'] = False
# Update the next break time
self.scheduled_next_break_time = self.scheduled_next_break_time + datetime.timedelta(seconds=self.postpone_duration)
self.__fire_on_update_next_break(self.scheduled_next_break_time)
# Wait in user thread
2020-03-18 13:33:11 +01:00
utility.start_thread(self.__postpone_break)
else:
self.start_break.fire(break_obj)
utility.start_thread(self.__start_break)
2017-10-07 15:10:31 +02:00
def __start_break(self):
2017-10-07 15:10:31 +02:00
"""
Start the break screen.
"""
self.context['state'] = State.BREAK
break_obj = self.break_queue.get_break()
countdown = break_obj.duration
2017-10-07 15:10:31 +02:00
total_break_time = countdown
while countdown and self.running and not self.context['skipped'] and not self.context['postponed']:
seconds = total_break_time - countdown
self.on_count_down.fire(countdown, seconds)
time.sleep(1) # Sleep for 1 second
countdown -= 1
2020-03-18 13:33:11 +01:00
utility.execute_main_thread(self.__fire_stop_break)
2017-10-07 15:10:31 +02:00
def __fire_stop_break(self):
# Loop terminated because of timeout (not skipped) -> Close the break alert
if not self.context['skipped'] and not self.context['postponed']:
logging.info("Break is terminated automatically")
self.on_stop_break.fire()
# Reset the skipped flag
self.context['skipped'] = False
self.__start_next_break()
def __wait_for(self, duration):
"""
Wait until someone wake up or the timeout happens.
"""
self.waiting_condition.acquire()
self.waiting_condition.wait(duration)
self.waiting_condition.release()
def __start_next_break(self):
if not self.context['postponed']:
self.break_queue.next()
2017-10-07 15:10:31 +02:00
if self.running:
# Schedule the break again
2020-03-18 13:33:11 +01:00
utility.start_thread(self.__scheduler_job)