Konubinix' opinionated web of thoughts

More Clever Night Light in Kivy

Fleeting

Following more clever night light, but using my a python runtime on android, so that I can use it when travelling. I also needed it a day when my sd card burned and I did not have a fiber connection to build a new raspberry pi image easily.

Using the click sound from click.ogg

import os
from datetime import datetime, timedelta

import math
import time
import requests
from subprocess import call
from kivy.app import App
from kivy.clock import Clock
from kivy.graphics import Color, Rectangle
import requests
from kivy.properties import BooleanProperty, NumericProperty, StringProperty
from kivy.uix.boxlayout import BoxLayout
from plyer import wifi
from android.runnable import run_on_ui_thread

import subprocess
from kivy.uix.spinner import Spinner
from plyer import orientation
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
from kivy.core.audio import SoundLoader

from kivy.uix.label import Label
from kivy.uix.progressbar import ProgressBar
from kivy.uix.screenmanager import Screen, ScreenManager, ScreenManagerException
from kivy.core.audio import SoundLoader

from jnius import autoclass

from helpers.osc import oschandler, to_service
from helpers.wakelock import WakeLock
from helpers.notify import notify, show_toast
from android.runnable import run_on_ui_thread
from logging import getLogger
from kivy.lang import Builder
from kivy.uix.togglebutton import ToggleButton

Context = autoclass('android.content.Context')
AlarmManager = autoclass('android.app.AlarmManager')
PendingIntent = autoclass('android.app.PendingIntent')
Intent = autoclass('android.content.Intent')
SystemClock = autoclass('android.os.SystemClock')
PythonActivity = autoclass('org.kivy.android.PythonActivity')
Build_VERSION = autoclass('android.os.Build$VERSION')
api_level = Build_VERSION.SDK_INT

logger = getLogger(__name__)

pink = (1, 0.75, 0.8)
orange = (1, 0.5, 0)
cyan = (0, 1, 1)
white = (1, 1, 1)
red = (1, 0, 0)
black = (0, 0, 0)
green = (0, 1, 0)

school_schedule = {
    "almost_hour": 6,
    "almost_minute": 30,
    "getup_hour": 7,
    "getup_minute": 0,
    "day_hour": 7,
    "day_minute": 40,
    "evening_hour": 20,
    "evening_minute": 0,
    "night_hour": 20,
    "night_minute": 30,
}

holiday_schedule = {
    "almost_hour": 7,
    "almost_minute": 30,
    "getup_hour": 8,
    "getup_minute": 0,
    "day_hour": 8,
    "day_minute": 40,
    "evening_hour": 20,
    "evening_minute": 0,
    "night_hour": 20,
    "night_minute": 30,
}

force_today = None

def ease_debugging(schedule, today=None, duration=30, start_time="night"):
    now = datetime.now()
    global force_today
    force_today = today

    start_assoc = {"night": 0, "almost": 1, "getup": 2, "day": 3, "evening": 4,
                   "tomorrow": 5,}

    schedule["almost_hour"] = (now + timedelta(minutes=duration*(1 - start_assoc[start_time]))).hour
    schedule["almost_minute"] = (now + timedelta(minutes=duration*(1 - start_assoc[start_time]))).minute

    schedule["getup_hour"] = (now + timedelta(minutes=duration*(2 - start_assoc[start_time]))).hour
    schedule["getup_minute"] = (now + timedelta(minutes=duration*(2 - start_assoc[start_time]))).minute

    schedule["day_hour"] = (now + timedelta(minutes=duration*(3 - start_assoc[start_time]))).hour
    schedule["day_minute"] = (now + timedelta(minutes=duration*(3 - start_assoc[start_time]))).minute

    schedule["evening_hour"] = (now + timedelta(minutes=duration*(4 - start_assoc[start_time]))).hour
    schedule["evening_minute"] = (now + timedelta(minutes=duration*(4 - start_assoc[start_time]))).minute

    schedule["night_hour"] = (now + timedelta(minutes=duration*(5 - start_assoc[start_time]))).hour
    schedule["night_minute"] = (now + timedelta(minutes=duration*(5 - start_assoc[start_time]))).minute

#ease_debugging(holiday_schedule, duration=60)
#ease_debugging(school_schedule, duration=60)

@run_on_ui_thread
def fs2():
    from jnius import autoclass, cast

    # Get the current Android activity and window
    PythonActivity = autoclass('org.kivy.android.PythonActivity')
    activity = PythonActivity.mActivity
    window = activity.getWindow()

    # Get decor view
    decorView = window.getDecorView()

    # View constants for immersive mode
    View = autoclass('android.view.View')
    flags = (
        View.SYSTEM_UI_FLAG_LAYOUT_STABLE
        | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
        | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
        | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
        | View.SYSTEM_UI_FLAG_FULLSCREEN
        | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
    )

    # Apply flags
    decorView.setSystemUiVisibility(flags)

    # Also set window flag to keep fullscreen
    LayoutParams = autoclass('android.view.WindowManager$LayoutParams')
    window.addFlags(LayoutParams.FLAG_FULLSCREEN)


def su(cmd):
    logger.debug("su: {}".format(cmd))
    call(["su", "-c", cmd])

def set_wifi_enabled(enable=True):
    if api_level >= 26:
        # on recent android, we cannot deal with the wifi anymore, I will
        # therefore rely on su and for sake of simplicity enable airplane mode
        # all together
        su("settings put global airplane_mode_on {}".format("0" if enable else "1"))
        su("am broadcast -a android.intent.action.AIRPLANE_MODE --ez state {}".format("false" if enable else "true"))
    else:
        # this works with android 4 and 5. I only need to play with the wifi
        # because all other services are generally already off on such legacy
        # devices
        from android import mActivity

        Context = autoclass('android.content.Context')
        WifiManager = autoclass('android.net.wifi.WifiManager')

        wifi_service = mActivity.getSystemService(Context.WIFI_SERVICE)

        wifi_service.setWifiEnabled(enable)
        wifi_service.reconnect()

def wake_n_notify(message, sleep=True):
    set_wifi_enabled(True)
    now = datetime.now()
    message = "{:02}:{:02}:{:02}: {}".format(now.hour, now.minute, now.second, message)
    logger.debug(message)
    show_toast(message)
    for _ in range(3):
        if notify(message, channel="nightlight"):
            show_toast("notification sent: {}".format(message), long=True)
            # also sync the date until I get back internet (and ntp) working
            try:
                if os.path.exists("/system/bin/toybox"):
                    resp = requests.get("http://home/dateserver/toybox")
                    if resp.status_code // 100 != 2:
                        raise NotImplementedError()
                    su("toybox date {}".format(resp.text))
                elif os.path.exists("/system/bin/toolbox"):
                    # heuristic. On the device that accepts another format, which is not there
                    if os.path.exists("/system/xbin/which"):
                        resp = requests.get("http://home/dateserver/toolbox1")
                        if resp.status_code // 100 != 2:
                            raise NotImplementedError()
                        su("toolbox date {}".format(resp.text))
                    else:
                        resp = requests.get("http://home/dateserver/toolbox2")
                        if resp.status_code // 100 != 2:
                            raise NotImplementedError()
                        su("toolbox date -s {}".format(resp.text))
                else:
                    raise NotImplementedError()
            except Exception as e:
                show_toast("Failed to update the date...: {}".format(e))
            break
        else:
            seconds = 5
            show_toast("retrying in {} seconds".format(seconds))
            time.sleep(seconds)
    if sleep:
        pass
        set_wifi_enabled(False)

class SettingsScreen(Screen):
    steps = NumericProperty(4)
    step_time = NumericProperty(20)
    rest_time = NumericProperty(10)
    total_time = StringProperty("Total: 0s")

    def _update_rect(self, instance, value):
        self.rect.pos = instance.pos
        self.rect.size = instance.size

    def get_current_schedule(self):
        if self.holidays.state == "down":
            return holiday_schedule
        else:
            return school_schedule

    def trigger_ease_debugging(self, *_):
        """Trigger ease_debugging on the currently selected schedule"""
        if self.debug_button.state == "normal":
            logger.info("Debug mode is off, not triggering ease_debugging")
            return
        ease_debugging(school_schedule, duration=int(self.debug_number.text))
        ease_debugging(holiday_schedule, duration=int(self.debug_number.text))
        logger.info("Triggered ease_debugging on current schedule")
        # Update the UI to reflect the new schedule values
        self.setup_times()

    def create_time_picker(self, layout, label_text, hour_key, minute_key):
        """Create a time picker layout with hour and minute spinners"""
        time_layout = BoxLayout(size_hint=(1, 0.15))
        layout.add_widget(time_layout)

        time_layout.add_widget(Label(text=label_text, color=(0, 0, 0, 1)))

        # Hour spinner
        hour_spinner = Spinner(
            text="0",  # Temporary value, will be set by setup_times
            values=[str(i) for i in range(0, 24)],
            size_hint=(None, None),
            size=(200, 44),
            pos_hint={'center_x': .5, 'center_y': .5}
        )
        self.bind_spinner(hour_spinner, hour_key)
        time_layout.ad
    def bind_spinner(self, spinner, key):
        def update_schedule(instance, value):
            current_schedule = self.get_current_schedule()
            current_schedule[key] = int(value)
            # logger.info("Updated {} to {} in global schedule".format(key, value))

        spinner.bind(text=update_schedule)

    def __init__(self, **kwargs):
        wake_n_notify("Updating the date", sleep=False)
        super(SettingsScreen, self).__init__(**kwargs)
        with self.canvas.before:
            self.color = Color(1, 1, 1, 1)
            self.rect = Rectangle(size=self.size, pos=self.pos)

        self.bind(size=self._update_rect, pos=self._update_rect)

        layout = BoxLayout(orientation='vertical')
        self.add_widget(layout)

        self.debug_button_layout = BoxLayout(size_hint=(1, 0.15))
        layout.add_widget(self.debug_button_layout)

        self.debug_button = ToggleButton(
            text='Debug Mode',
            size_hint=(1, 1),
            background_color=(1, 0.5, 0, 1),
            pos_hint={'center_x': .5, 'center_y': .5},
            color=(1, 1, 1, 1),
        )
        self.debug_button.bind(on_press=self.trigger_ease_debugging)
        self.debug_button_layout.add_widget(self.debug_button)
        self.debug_number = Spinner(
            text="1",
            values=[str(i) for i in range(0, 61)],  # numbers 0 to 60
            size_hint=(1, 1),
            pos_hint={'center_x': .5, 'center_y': .5},
        )
        self.debug_number.bind(text=self.trigger_ease_debugging)
        self.debug_button_layout.add_widget(self.debug_number)

        self.holidays = ToggleButton(text='Holidays', size_hint=(1, 0.15))

        layout.add_widget(self.holidays)
        self.holidays.bind(on_press=self.setup_times)

        almost_layout = BoxLayout(size_hint=(1, 0.15))
        layout.add_widget(almost_layout)
        almost_layout.add_widget(
            Label(text="Almost", color=(0, 0, 0, 1))
        )
        self.almost_hour = Spinner(
            text="7",
            values=[str(i) for i in range(0, 24)],  # numbers 0 to 100
            size_hint=(None, None),
            size=(200, 44),
            pos_hint={'center_x': .5, 'center_y': .5}
        )
        self.bind_spinner(self.almost_hour, "almost_hour")
        almost_layout.add_widget(self.almost_hour)
        self.almost_minute = Spinner(
            text='30',
            values=[str(i) for i in range(0, 60)],  # numbers 0 to 100
            size_hint=(None, None),
            size=(200, 44),
            pos_hint={'center_x': .5, 'center_y': .5}
        )
        self.bind_spinner(self.almost_minute, "almost_minute")
        almost_layout.add_widget(self.almost_minute)

        getup_layout = BoxLayout(size_hint=(1, 0.15))
        layout.add_widget(getup_layout)
        getup_layout.add_widget(
            Label(text="getup", color=(0, 0, 0, 1))
        )
        self.getup_hour = Spinner(
            text="8",
            values=[str(i) for i in range(0, 24)],  # numbers 0 to 100
            size_hint=(None, None),
            size=(200, 44),
            pos_hint={'center_x': .5, 'center_y': .5}
        )
        self.bind_spinner(self.getup_hour, "getup_hour")
        getup_layout.add_widget(self.getup_hour)
        self.getup_minute = Spinner(
            text='0',
            values=[str(i) for i in range(0, 60)],  # numbers 0 to 100
            size_hint=(None, None),
            size=(200, 44),
            pos_hint={'center_x': .5, 'center_y': .5}
        )
        self.bind_spinner(self.getup_minute, "getup_minute")
        getup_layout.add_widget(self.getup_minute)

        day_layout = BoxLayout(size_hint=(1, 0.15))
        layout.add_widget(day_layout)
        day_layout.add_widget(
            Label(text="day", color=(0, 0, 0, 1))
        )
        self.day_hour = Spinner(
            text="8",
            values=[str(i) for i in range(0, 24)],  # numbers 0 to 100
            size_hint=(None, None),
            size=(200, 44),
            pos_hint={'center_x': .5, 'center_y': .5}
        )
        self.bind_spinner(self.day_hour, "day_hour")
        day_layout.add_widget(self.day_hour)
        self.day_minute = Spinner(
            text='40',
            values=[str(i) for i in range(0, 60)],  # numbers 0 to 100
            size_hint=(None, None),
            size=(200, 44),
            pos_hint={'center_x': .5, 'center_y': .5}
        )
        self.bind_spinner(self.day_minute, "day_minute")
        day_layout.add_widget(self.day_minute)

        evening_layout = BoxLayout(size_hint=(1, 0.15))
        layout.add_widget(evening_layout)
        evening_layout.add_widget(
            Label(text="evening", color=(0, 0, 0, 1))
        )
        self.evening_hour = Spinner(
            text="20",
            values=[str(i) for i in range(0, 24)],  # numbers 0 to 100
            size_hint=(None, None),
            size=(200, 44),
            pos_hint={'center_x': .5, 'center_y': .5}
        )
        self.bind_spinner(self.evening_hour, "evening_hour")
        evening_layout.add_widget(self.evening_hour)
        self.evening_minute = Spinner(
            text='0',
            values=[str(i) for i in range(0, 60)],  # numbers 0 to 100
            size_hint=(None, None),
            size=(200, 44),
            pos_hint={'center_x': .5, 'center_y': .5}
        )
        self.bind_spinner(self.evening_minute, "evening_minute")
        evening_layout.add_widget(self.evening_minute)

        night_layout = BoxLayout(size_hint=(1, 0.15))
        layout.add_widget(night_layout)
        night_layout.add_widget(
            Label(text="night", color=(0, 0, 0, 1))
        )
        self.night_hour = Spinner(
            text="8",
            values=[str(i) for i in range(0, 24)],  # numbers 0 to 100
            size_hint=(None, None),
            size=(200, 44),
            pos_hint={'center_x': .5, 'center_y': .5}
        )
        self.bind_spinner(self.night_hour, "night_hour")
        night_layout.add_widget(self.night_hour)
        self.night_minute = Spinner(
            text='0',
            values=[str(i) for i in range(0, 60)],  # numbers 0 to 100
            size_hint=(None, None),
            size=(200, 44),
            pos_hint={'center_x': .5, 'center_y': .5}
        )
        self.bind_spinner(self.night_minute, "night_minute")
        night_layout.add_widget(self.night_minute)

        self.setup_times()

        self.go_button = Button(text="Go !!!",
                           background_color=(0, 0, 1, 1),
                           color=(1, 1, 1, 1),
                           size_hint=(1, 0.15), disabled=True)
        self.go_button.bind(on_press=self.start)
        layout.add_widget(self.go_button)

        @oschandler("service:ready")
        def _(_):
            self.go_button.disabled = False

    def setup_times(self, instance=None):
        if self.holidays.state == "down":
            logger.info("Using holidays schedule {}".format(self.holidays.state))
            schedule = holiday_schedule
        else:
            logger.info("Using school schedule")
            schedule = school_schedule

        self.almost_hour.text = str(schedule["almost_hour"])
        self.almost_minute.text = str(schedule["almost_minute"])
        self.getup_hour.text = str(schedule["getup_hour"])
        self.getup_minute.text = str(schedule["getup_minute"])
        self.day_hour.text = str(schedule["day_hour"])
        self.day_minute.text = str(schedule["day_minute"])
        self.evening_hour.text = str(schedule["evening_hour"])
        self.evening_minute.text = str(schedule["evening_minute"])
        self.night_hour.text = str(schedule["night_hour"])
        self.night_minute.text = str(schedule["night_minute"])


    def start(self, *_):
        app = App.get_running_app()
        normal = app.sm.get_screen('nightlight:normal')
        normal.debug = self.debug_button.state == "down"
        normal.debug_number = int(self.debug_number.text)
        normal.holidays = self.holidays.state == "down"

        app.goto("nightlight:normal")
        normal.start()

class NormalScreen(Screen):

    def _update_rect(self, instance, value):
        self.rect.pos = instance.pos
        self.rect.size = instance.size

    def _get_time(self, value):
        now = datetime.now()
        today = force_today or now.weekday()
        if self.holidays or today in [2, 5, 6]:  # 2=Wednesday, 5=Saturday, 6=Sunday
            schedule = holiday_schedule
            logger.debug("the schedule of today is holiday")
        else:
            schedule = school_schedule
            logger.debug("the schedule of today is school")
        if now.replace(hour=schedule["night_hour"],
                       minute=schedule["night_minute"], second=0,) < now:
            logger.debug("it is late, I should consider the schedule of tomorrow")
            if self.holidays or today in [2-1, 5-1, 6-1]:  # tomorrow is a day off
                schedule = holiday_schedule
            else:
                schedule = school_schedule
        if value == "almost_hour" and self.debug:
            ease_debugging(schedule, duration=self.debug_number)
        logger.debug("Today: {}, schedule[{}]: {}".format(today, value, schedule[value]))
        return schedule[value]

    @property
    def almost_hour(self):
        return self._get_time("almost_hour")
    @property
    def almost_minute(self):
        return self._get_time("almost_minute")
    @property
    def getup_hour(self):
        return self._get_time("getup_hour")
    @property
    def getup_minute(self):
        return self._get_time("getup_minute")
    @property
    def day_hour(self):
        return self._get_time("day_hour")
    @property
    def day_minute(self):
        return self._get_time("day_minute")
    @property
    def evening_hour(self):
        return self._get_time("evening_hour")
    @property
    def evening_minute(self):
        return self._get_time("evening_minute")
    @property
    def night_hour(self):
        return self._get_time("night_hour")

    @property
    def night_minute(self):
        return self._get_time("night_minute")

    def __init__(self, **kwargs):
        super(NormalScreen, self).__init__(**kwargs)
        self.click = SoundLoader.load(os.path.join(os.path.dirname(__file__), "click.ogg"))
        self.remaining_time = None

        with self.canvas.before:
            self.color = Color(0, 0, 0, 1)
            self.rect = Rectangle(size=self.size, pos=self.pos)

        self.bind(size=self._update_rect, pos=self._update_rect)

        layout = BoxLayout(orientation='vertical')
        self.add_widget(layout)

        current_layout = BoxLayout(size_hint=(1, 0.15))
        layout.add_widget(current_layout)
        self.current_hour = Label(
            text="7",
            font_size="115sp",
        )
        current_layout.add_widget(self.current_hour)
        self.sep = Label(
            text=":",
            font_size="150sp",
        )
        current_layout.add_widget(self.sep)
        self.current_minute = Label(
            text='30',
            font_size="115sp",
        )
        current_layout.add_widget(self.current_minute)

        self.keepon = WakeLock("nightlight:keepon", wakeup=False, partial=True)
        self.wakeup = WakeLock("nightlight:wakeup", wakeup=True, partial=False)
        self.keepon.acquire()
        self.update_display()

        @oschandler("clockwake:done:nightlight:almost", answer=True)
        def _(_):
            app = App.get_running_app()
            if app.sm.current == self.name:
                Clock.schedule_once(self.almost)
            else:
                wake_n_notify("nightlight:almost, but not in the right screen")

        @oschandler("clockwake:done:nightlight:getup", answer=True)
        def _(_):
            app = App.get_running_app()
            if app.sm.current == self.name:
                Clock.schedule_once(self.getup)
            else:
                wake_n_notify("nightlight:getup, but not in the right screen")

        @oschandler("clockwake:done:nightlight:day", answer=True)
        def _(_):
            app = App.get_running_app()
            if app.sm.current == self.name:
                Clock.schedule_once(self.day)
            else:
                wake_n_notify("nightlight:day, but not in the right screen")

        @oschandler("clockwake:done:nightlight:evening", answer=True)
        def _(_):
            app = App.get_running_app()
            if app.sm.current == self.name:
                Clock.schedule_once(self.evening)
            else:
                wake_n_notify("nightlight:evening, but not in the right screen")

        @oschandler("clockwake:done:nightlight:night", answer=True)
        def _(_):
            app = App.get_running_app()
            if app.sm.current == self.name:
                Clock.schedule_once(self.night)
            else:
                wake_n_notify("nightlight:night, but not in the right screen")

        self.bind(on_pre_enter=self.pre_enter_handler)
        self.bind(on_pre_leave=self.pre_leave_handler)

    def pre_leave_handler(self, _):
        self.keepon.release()
        self.wakeup.release()
        from jnius import autoclass
        from android import mActivity

        View = autoclass('android.view.View')

        option = View.SYSTEM_UI_FLAG_VISIBLE
        from android.runnable import run_on_ui_thread

        @run_on_ui_thread
        def fs():
            logger.debug("Exit fullscreen mode")
            mActivity.getWindow().getDecorView().setSystemUiVisibility(option)

        if not self.debug:
            fs()

    def pre_enter_handler(self, _):
        # from plyer import orientation
        # orientation.set_landscape()

        from jnius import autoclass
        from android import mActivity

        View = autoclass('android.view.View')

        option = View.SYSTEM_UI_FLAG_FULLSCREEN

        @run_on_ui_thread
        def fs():
            logger.debug("Entering fullscreen mode")
            mActivity.getWindow().getDecorView().setSystemUiVisibility(option)

        if not self.debug:

            if api_level >= 26:
                fs2()
            else:
                fs()

    def update_display(self, *_):
        now = datetime.now()
        self.current_hour.text = str(now.hour).zfill(2)
        self.current_minute.text = str(now.minute).zfill(2)
        Clock.schedule_once(self.update_display, 61 - now.second)

    def durationuntil(self, hour, minute):
        now = datetime.now()
        new_date = now.replace(hour=hour, minute=minute,second=0)
        duration = (new_date - now).total_seconds()
        if duration < 0:
            new_date = new_date + timedelta(days=1)
            duration = (new_date - now).total_seconds()
        return duration

    def start(self):
        now = datetime.now()
        almost = now.replace(hour=self.almost_hour, minute=self.almost_minute)
        getup = now.replace(hour=self.getup_hour, minute=self.getup_minute)
        day = now.replace(hour=self.day_hour, minute=self.day_minute)
        evening = now.replace(hour=self.evening_hour, minute=self.evening_minute)
        night = now.replace(hour=self.night_hour, minute=self.night_minute)

        if now < almost:
            self.night()
        elif now < getup:
            self.almost()
        elif now < day:
            self.getup()
        elif now < evening:
            self.day()
        elif now < night:
            self.evening()
        else:
            self.night()

    def almost(self, *_):
        self.color.rgb = orange
        self.current_hour.color = (0,0,0, 1)
        self.current_minute.color = (0,0,0, 1)
        self.sep.color = (0,0,0, 1)
        self.wakeup.acquire()
        wake_n_notify("almost time, next at {:02}:{:02}".format(self.getup_hour, self.getup_minute))
        to_service("clockwake", {"duration": self.durationuntil(self.getup_hour, self.getup_minute), "callback": "nightlight:getup"})

    def getup(self, *_):
        self.color.rgb = green
        self.current_hour.color = (0,0,0, 1)
        self.sep.color = (0,0,0, 1)
        self.current_minute.color = (0,0,0, 1)
        self.wakeup.acquire()
        wake_n_notify("getup, next at {:02}:{:02}".format(self.day_hour, self.day_minute))
        self.click.seek(0)
        self.click.play()
        to_service("clockwake", {"duration": self.durationuntil(self.day_hour, self.day_minute), "callback": "nightlight:day"})

    def day(self, *_):
        wake_n_notify("day, next at {:02}:{:02}".format(self.evening_hour, self.evening_minute))
        self.wakeup.release()
        self.current_hour.color = (0,0,0, 1)
        self.sep.color = (0,0,0, 1)
        self.current_minute.color = (0,0,0, 1)
        self.color.rgb = white
        to_service("clockwake", {"duration": self.durationuntil(self.evening_hour, self.evening_minute), "callback": "nightlight:evening"})

    def evening(self, *_):
        self.color.rgb = red
        self.current_hour.color = (1,1,1, 1)
        self.sep.color = (1,1,1, 1)
        self.current_minute.color = (1,1,1, 1)
        self.wakeup.acquire()
        wake_n_notify("evening, next at {:02}:{:02}".format(self.night_hour, self.night_minute))
        self.click.seek(0)
        self.click.play()
        to_service("clockwake", {"duration": self.durationuntil(self.night_hour, self.night_minute), "callback": "nightlight:night"})

    def night(self, *_):
        wake_n_notify("night, next at {:02}:{:02}".format(self.almost_hour, self.almost_minute))
        self.wakeup.release()
        self.color.rgb = black
        to_service("clockwake", {"duration": self.durationuntil(self.almost_hour, self.almost_minute), "callback": "nightlight:almost"})


class NightLightApp(App):

    def goto(self, screen):
        self.sm.current = screen

    def back(self):
        self.sm.current = 'nightlight:normal'

    @staticmethod
    def populate(sm):
        try:
            sm.get_screen('nightlight:normal')
            return
        except ScreenManagerException:
            pass # not populated yet

        sm.add_widget(SettingsScreen(name='nightlight:settings'))
        sm.add_widget(NormalScreen(name='nightlight:normal'))

    def build(self):
        self.sm = ScreenManager()
        self.populate(self.sm)
        return self.sm


def run():
    NightLightApp().run()

Notes linking here