# Many thanks to https://stackoverflow.com/questions/18923321/making-a-clock-in-kivy import collections import datetime import math import sys import traceback from kivy.app import App from kivy.uix.floatlayout import FloatLayout from kivy.uix.label import Label from kivy.clock import Clock from kivy.lang import Builder from kivy.graphics import Color, Line, Rectangle from multiprocessing import Process from playsound import playsound Builder.load_string(''' : on_pos: self.update_clock() on_size: self.update_clock() FloatLayout id: face size_hint: None, None pos_hint: {"center_x":0.5, "center_y":0.5} size: 0.9*min(root.size), 0.9*min(root.size) canvas: Color: rgb: 0.1, 0.1, 0.1 Ellipse: size: self.size pos: self.pos FloatLayout id: hands size_hint: None, None pos_hint: {"center_x":0.5, "center_y":0.5} size: 0.9*min(root.size), 0.9*min(root.size) FloatLayout id: set_alarm_button size_hint: None, None pos_hint: {"center_x":0.9, "center_y":0.1} size: 0.1*min(root.size), 0.1*min(root.size) ''') Position = collections.namedtuple('Position', 'x y') global sound_process sound_process = None def play_sound(source): playsound(source) class MyClockWidget(FloatLayout): set_alarm_mode = False alarm_time = datetime.datetime(2022, 12, 10, 7, 30, 0, 0) alarm_activated = False grabbed = "" face_numbers = [] set_alarm_timeout_counter = 0 alarm_modified = False seconds_to_next_alarm = False led_color = [0, 0, 0] # sound_source = "https://icecast.omroep.nl/radio1-bb-mp3" sound_source = "Woodpecker Chirps - QuickSounds.com.mp3" def draw_face(self): """ Add number labels when added in widget hierarchy """ if self.set_alarm_mode: time = self.alarm_time else: time = datetime.datetime.now() for i in range(1, 13): if time.hour < 12: offset = 0 else: offset = 12 self.face_numbers.append(Label( text=str(i + offset), pos_hint={ # pos_hint is a fraction in range (0, 1) "center_x": 0.5 + 0.45*math.sin(2 * math.pi * i/12), "center_y": 0.5 + 0.45*math.cos(2 * math.pi * i/12), } )) self.ids["face"].add_widget(self.face_numbers[i - 1]) def update_face(self): if self.set_alarm_mode: time = self.alarm_time else: time = datetime.datetime.now() for i in range(0, 12): if time.hour < 12: offset = 0 else: offset = 12 self.face_numbers[i].text=str(i + 1 + offset) def on_parent(self, myclock, parent): self.draw_face() def position_on_clock(self, fraction, length): """ Calculate position in the clock using trygonometric functions """ center_x = self.size[0]/2 center_y = self.size[1]/2 return Position( center_x + length * math.sin(2 * math.pi * fraction), center_y + length * math.cos(2 * math.pi * fraction), ) def update_set_alarm_button(self): set_alarm_button = self.ids["set_alarm_button"] if self.set_alarm_mode or self.alarm_activated: source = 'alarm_on.png' r = 0.9 g = 0.0 b = 0.0 else: source = 'alarm_off.png' r = 1.0 g = 1.0 b = 1.0 set_alarm_button = self.ids["set_alarm_button"] set_alarm_button.canvas.clear() with set_alarm_button.canvas: Color(r, g, b) Rectangle(size=set_alarm_button.size, pos=set_alarm_button.pos, source=source) def sun_rise(self): # to do: calculate brightness and color according to sun rise instead of linear increment intensity = round((1.0 - self.seconds_to_next_alarm / (30.0 * 60.0)) * 256.0) led_color = [0, 0, 0] for i in range(3): led_color[i] = intensity if self.led_color != led_color: self.led_color = led_color def check_sun_rise(self): if self.alarm_activated == False: return if self.seconds_to_next_alarm < 30 * 60: self.sun_rise() def check_play_sound(self): global sound_process if self.alarm_activated == False: return if self.seconds_to_next_alarm < 0.1 and sound_process is None: print("beep beep!") sound_process = Process(target=play_sound, args=(self.sound_source,)) sound_process.start() def calc_seconds_to_next_alarm(self): if self.alarm_activated == False: return # Make sure alarm_time is in the future but not more than 24 h from now now = datetime.datetime.now() d = self.alarm_time - now self.alarm_time -= datetime.timedelta(days=d.days) # Calculate number of seconds until next alarm d = self.alarm_time - now self.seconds_to_next_alarm = d.days * 24 * 3600 + d.seconds + d.microseconds / 1000000.0 def check_alarm(self): self.calc_seconds_to_next_alarm() self.check_sun_rise() self.check_play_sound() def update_clock(self, *args): self.check_alarm() """ Redraw clock hands """ if self.set_alarm_mode: time = self.alarm_time else: time = datetime.datetime.now() hands = self.ids["hands"] seconds_hand = self.position_on_clock(time.second/60, length=0.45*hands.size[0]) minutes_hand = self.position_on_clock(time.minute/60+time.second/3600, length=0.40*hands.size[0]) hours_hand = self.position_on_clock(time.hour/12 + time.minute/720, length=0.35*hands.size[0]) self.update_face() self.update_set_alarm_button() hands.canvas.clear() with hands.canvas: if self.set_alarm_mode: if self.grabbed != "" or self.set_alarm_timeout_counter < 1 * 60 or self.set_alarm_timeout_counter % 60 <= 30 or self.alarm_modified == False: Color(0.9, 0.0, 0.0) Line(points=[hands.center_x, hands.center_y, hours_hand.x, hours_hand.y], width=3, cap="round") Color(0.8, 0.0, 0.0) Line(points=[hands.center_x, hands.center_y, minutes_hand.x, minutes_hand.y], width=2, cap="round") if self.grabbed == "": self.set_alarm_timeout_counter += 1 if self.set_alarm_timeout_counter >= 4 * 60 + 30: self.set_alarm_mode = False self.set_alarm_timeout_counter = 0 if self.alarm_modified: self.alarm_activated = True self.alarm_modified = False else: Color(0.9, 0.9, 0.9) Line(points=[hands.center_x, hands.center_y, hours_hand.x, hours_hand.y], width=3, cap="round") Color(0.8, 0.8, 0.8) Line(points=[hands.center_x, hands.center_y, minutes_hand.x, minutes_hand.y], width=2, cap="round") Color(0.7, 0.7, 0.7) Line(points=[hands.center_x, hands.center_y, seconds_hand.x, seconds_hand.y], width=1, cap="round") def on_alarm_button_pressed(self): self.alarm_modified = False self.set_alarm_timeout_counter = 0 if self.set_alarm_mode: self.set_alarm_mode = False self.alarm_activated = False else: self.set_alarm_mode = True self.alarm_activated = True def on_touch_up(self, touch): self.grabbed = "" if self.set_alarm_mode and (self.grabbed == "hour" or self.grabbed == "minute"): self.set_alarm_timeout_counter = 0 def on_touch_move(self, touch): self.alarm_set_timeout = 0 x = touch.pos[0] - self.size[0]/2 y = touch.pos[1] - self.size[1]/2 angle = math.atan2(y, x) / math.pi; # angle is between -1 and 1 if self.grabbed == "minute": self.alarm_modified = True self.set_alarm_timeout_counter = 0 minute = round(-angle * 30 + 15) if minute < 0: minute += 60 if minute == 60: minute = 59 # hour correction hour = self.alarm_time.hour if self.alarm_time.minute >= 55 and minute <= 5: hour += 1 elif self.alarm_time.minute <= 5 and minute >= 55: hour -= 1 if hour == 24: hour = 0 elif hour == -1: hour = 23 self.alarm_time = datetime.datetime(self.alarm_time.year, self.alarm_time.month, self.alarm_time.day, \ hour, minute, self.alarm_time.second, 0) elif self.grabbed == "hour": self.alarm_modified = True self.set_alarm_timeout_counter = 0 hour = round(-angle * 6 + 3) if hour < 0: hour += 12 if hour == 12: hour = 0 if self.alarm_time.hour >= 12: hour += 12 # AM / PM correction if self.alarm_time.hour == 11 and hour == 0: hour = 12 elif self.alarm_time.hour == 23 and hour == 12: hour = 0 elif self.alarm_time.hour == 0 and hour == 11: hour = 23 elif self.alarm_time.hour == 12 and hour == 23: hour = 11 # AM / PM boundary self.alarm_time = datetime.datetime(self.alarm_time.year, self.alarm_time.month, self.alarm_time.day, \ hour, self.alarm_time.minute, self.alarm_time.second, 0) def on_touch_down(self, touch): global sound_process time = self.alarm_time hands = self.ids["hands"] minutes_hand = self.position_on_clock(time.minute/60+time.second/3600, length=0.40*hands.size[0]) hours_hand = self.position_on_clock(time.hour/12 + time.minute/720, length=0.35*hands.size[0]) self.grabbed = "" if (0.85 <= touch.spos[0] <= 0.95) and (0.05 <= touch.spos[1] <= 0.15): self.on_alarm_button_pressed() elif sound_process is not None: kill_sound_process() elif self.set_alarm_mode: self.set_alarm_timeout_counter = 0 if (minutes_hand.x - 0.1 * self.size[0] <= touch.pos[0] <= minutes_hand.x + 0.1 * self.size[0]) and \ (minutes_hand.y - 0.1 * self.size[1] <= touch.pos[1] <= minutes_hand.y + 0.1 * self.size[1]): self.grabbed = "minute" elif (hours_hand.x - 0.1 * self.size[0] <= touch.pos[0] <= hours_hand.x + 0.1 * self.size[0]) and \ (hours_hand.y - 0.1 * self.size[1] <= touch.pos[1] <= hours_hand.y + 0.1 * self.size[1]): self.grabbed = "hour" class MyApp(App): def build(self): clock_widget = MyClockWidget() # update initially, just after construction of the widget is complete Clock.schedule_once(clock_widget.update_clock, 0) # then update 60 times per second Clock.schedule_interval(clock_widget.update_clock, 1.0/60.0) return clock_widget def kill_sound_process(): global sound_process if sound_process is not None: sound_process.kill() sound_process = None def except_hook(type, value, tb): kill_sound_process() if __name__ == '__main__': # sys.excepthook = except_hook MyApp().run()