# 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.uix.slider import Slider from kivy.uix.button import Button 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 import pulsectl from kivy.config import Config Config.set('graphics', 'width', '800') Config.set('graphics', 'height', '480') Builder.load_string(''' : on_pos: self.update_display() on_size: self.update_display() 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) FloatLayout id: settings_button size_hint: None, None pos_hint: {"center_x":0.9, "center_y":0.9} size: 0.1*310/560*min(root.size), 0.1*310/560*min(root.size) FloatLayout id: settings_menu size_hint: None, None pos_hint: {"center_x":0.5, "center_y":0.5} size: 0.7*min(root.size), 0.7*min(root.size) canvas: Color: rgb: 0.1, 0.1, 0.1 Rectangle: size: self.size pos: self.pos GridLayout: rows: 8 cols: 2 pos_hint: {"center_x":0.5, "center_y":0.5} Label: text: "Volume" halign: "left" valign: "middle" text_size: self.size Slider: id: volume_slider min: 0 max: 20 value: 15 on_value: root.volume_slider_value(*args) value_track: True value_track_color: [1, 0, 0, 1] Label: text:"Wake up sound" halign: "left" valign: "middle" text_size: self.size Button: id: settings_menu_wake_up_sound_select_button on_press: root.settings_menu_wake_up_sound_select_button_cb() text: str(app.alarm_settings.sound_selected) Label: text:"Wake up brightness" halign: "left" valign: "middle" text_size: self.size Slider: id: wake_up_bightness_slider min: 0 max: 20 value: 20 on_value: root.wake_up_brightness_slider_value(*args) value_track: True value_track_color: [1, 0, 0, 1] Label: text:"Reading light brightness" halign: "left" valign: "middle" text_size: self.size Slider: id: reading_light_brightness_slider min: 0 max: 20 value: 1 on_value: root.reading_light_brightness_slider_value(*args) value_track: True value_track_color: [1, 0, 0, 1] Label: text:"Display brightness" halign: "left" valign: "middle" text_size: self.size Slider: id: display_brightness_slider min: 0 max: 20 value: 10 on_value: root.display_brightness_slider_value(*args) value_track: True value_track_color: [1, 0, 0, 1] Label: text:"Wifi network" halign: "left" valign: "middle" text_size: self.size Label: text:"wifi name" halign: "left" valign: "middle" text_size: self.size Label: text:"Wifi password" halign: "left" valign: "middle" text_size: self.size Label: text:"wifi password" halign: "left" valign: "middle" text_size: self.size Label: text:"Calendar URL" halign: "left" valign: "middle" text_size: self.size Label: text:"calendar url" halign: "left" valign: "middle" text_size: self.size FloatLayout id: settings_menu_wake_up_sound size_hint: None, None pos_hint: {"center_x":0.5, "center_y":0.5} size: 0.7*min(root.size), 0.7*min(root.size) canvas: Color: rgb: 0.1, 0.1, 0.1 Rectangle: size: self.size pos: self.pos BoxLayout: orientation: "vertical" pos_hint: {"center_x":0.5, "center_y":0.5} Label: text: "Select wake up sound" GridLayout: cols: 2 CheckBox: group: "settings_menu_wake_up_sound" on_active: root.settings_menu_wake_up_sound_cb(self, self.active, "Birds") id: settings_menu_wake_up_sound_Birds Label: text: "Birds" halign: "left" valign: "middle" text_size: self.size active: True CheckBox: group: "settings_menu_wake_up_sound" on_active: root.settings_menu_wake_up_sound_cb(self, self.active, "NPO Radio 1") id: settings_menu_wake_up_sound_NpoRadio1 Label: text: "NPO Radio 1" halign: "left" valign: "middle" text_size: self.size Button: id: settings_menu_wake_up_sound_Ok_button on_press: root.settings_menu_wake_up_sound_Ok_button_cb() text: "Ok" ''') Position = collections.namedtuple('Position', 'x y') global sound_process sound_process = None def play_sound(source): while True: print("beep beep!") playsound(source) class AlarmSettings(): alarm_time = datetime.datetime(2022, 12, 10, 7, 30, 0, 0) alarm_activated = False alarm_modified = False led_color = [0, 0, 0] # sound_selected = "NPO Radio 1" # sound_source = "https://icecast.omroep.nl/radio1-bb-mp3" sound_selected = "Birds" sound_source = "Woodpecker Chirps - QuickSounds.com.mp3" seconds_to_sunrise = 30 * 60 # 30 minutes volume = 15 wake_up_brightness = 20 reading_light_brightness = 1 display_brightness = 10 class MyClockWidget(FloatLayout): grabbed = "" face_numbers = [] # if not grabbed, set_alarm_timeout_counter is incremented at every update call; # this is used to blink the hands at 1 Hz when setting the alarm and releasing the hand set_alarm_timeout_counter = 0 seconds_to_next_alarm = 0 led_color = [0, 0, 0] # view can be one of the following strings: # - "clock" # - "set_alarm" # - "settings_menu" # - "settings_menu_wake_up_sound" # - "calendar" view = "clock" # we need a dirty hack to sensure that radio button is always in sync with selected setting settings_menu_wake_up_sound_select_button_cb_hack = False def hide_widget(self, widget, hide=True): if hasattr(widget, 'saved_attrs'): if not hide: widget.height, widget.size_hint_y, widget.opacity, widget.disabled = widget.saved_attrs del widget.saved_attrs elif hide: widget.saved_attrs = widget.height, widget.size_hint_y, widget.opacity, widget.disabled widget.height, widget.size_hint_y, widget.opacity, widget.disabled = 0, None, 0, True def draw_face(self): """ Add number labels when added in widget hierarchy """ alarm_settings = App.get_running_app().alarm_settings if self.view == "set_alarm": time = alarm_settings.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): alarm_settings = App.get_running_app().alarm_settings if self.view == "set_alarm": time = alarm_settings.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): alarm_settings = App.get_running_app().alarm_settings if (self.view == "set_alarm") or alarm_settings.alarm_activated: source = 'alarm_on.png' rgb = [0.9, 0.0, 0.0] else: source = 'alarm_off.png' rgb = [1.0, 1.0, 1.0] set_alarm_button = self.ids["set_alarm_button"] set_alarm_button.canvas.clear() with set_alarm_button.canvas: Color(rgb[0], rgb[1], rgb[2]) Rectangle(size=set_alarm_button.size, pos=set_alarm_button.pos, source=source) def update_settings_button(self): alarm_settings = App.get_running_app().alarm_settings if (self.view.startswith("settings_menu")): source = 'settings_visible.png' rgb = [0.9, 0.0, 0.0] else: source = 'settings_not_visible.png' rgb = [1.0, 1.0, 1.0] settings_button = self.ids["settings_button"] settings_button.canvas.clear() with settings_button.canvas: Color(rgb[0], rgb[1], rgb[2]) Rectangle(size=settings_button.size, pos=settings_button.pos, source=source) def sun_rise(self): alarm_settings = App.get_running_app().alarm_settings # to do: calculate brightness and color according to sun rise instead of linear increment intensity = math.floor((1.0 - self.seconds_to_next_alarm / alarm_settings.seconds_to_sunrise) * 256.0) if intensity < 0: intensity = 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): alarm_settings = App.get_running_app().alarm_settings if alarm_settings.alarm_activated == False: return if self.seconds_to_next_alarm < alarm_settings.seconds_to_sunrise: self.sun_rise() def check_play_sound(self): global sound_process alarm_settings = App.get_running_app().alarm_settings if alarm_settings.alarm_activated == False: return if self.seconds_to_next_alarm < 0.1: print("Rise and shine baby!" + str(alarm_settings.sound_source)) if alarm_settings.sound_source != "" and sound_process is None: sound_process = Process(target=play_sound, args=(alarm_settings.sound_source,)) sound_process.start() def calc_seconds_to_next_alarm(self): alarm_settings = App.get_running_app().alarm_settings if alarm_settings.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 = alarm_settings.alarm_time - now alarm_settings.alarm_time -= datetime.timedelta(days=d.days) # Calculate number of seconds until next alarm d = alarm_settings.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): self.hide_widget(self.ids["face"], False) self.hide_widget(self.ids["hands"], False) alarm_settings = App.get_running_app().alarm_settings if self.view == "set_alarm": time = alarm_settings.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() hands.canvas.clear() update_rate = App.get_running_app().update_rate with hands.canvas: if self.view == "set_alarm": if self.grabbed != "" or self.set_alarm_timeout_counter < 1 * update_rate or \ self.set_alarm_timeout_counter % update_rate <= update_rate / 2 or alarm_settings.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.5 * update_rate: self.view = "clock" self.set_alarm_timeout_counter = 0 if alarm_settings.alarm_modified: alarm_settings.alarm_activated = True alarm_settings.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 update_settings(self): if self.view == "settings_menu": self.hide_widget(self.ids["settings_menu"], False) elif self.view == "settings_menu_wake_up_sound": self.hide_widget(self.ids["settings_menu_wake_up_sound"], False) def update_display(self, *args): self.check_alarm() # Hide all dynamic widgets; will be enabled when updating respecive view self.hide_widget(self.ids["face"], True) self.hide_widget(self.ids["hands"], True) self.hide_widget(self.ids["settings_menu"], True) self.hide_widget(self.ids["settings_menu_wake_up_sound"], True) self.update_set_alarm_button() self.update_settings_button() if self.view == "clock" or self.view == "set_alarm": self.update_clock() elif self.view.startswith("settings_menu"): self.update_settings() def settings_menu_wake_up_sound_select_button_cb(self): self.settings_menu_wake_up_sound_select_button_cb_hack = True alarm_settings = App.get_running_app().alarm_settings self.ids["settings_menu_wake_up_sound_Birds"].active = False self.ids["settings_menu_wake_up_sound_NpoRadio1"].active = False print("sound selected: " + alarm_settings.sound_selected) if alarm_settings.sound_selected == "Birds": self.ids["settings_menu_wake_up_sound_Birds"].active = True elif alarm_settings.sound_selected == "NPO Radio 1": self.ids["settings_menu_wake_up_sound_NpoRadio1"].active = True self.view = "settings_menu_wake_up_sound" def settings_menu_wake_up_sound_Ok_button_cb(self): alarm_settings = App.get_running_app().alarm_settings self.ids["settings_menu_wake_up_sound_select_button"].text = alarm_settings.sound_selected self.view = "settings_menu" def settings_menu_wake_up_sound_cb(self, instance, value, sound): alarm_settings = App.get_running_app().alarm_settings if self.settings_menu_wake_up_sound_select_button_cb_hack: self.settings_menu_wake_up_sound_select_button_cb_hack = False if not (alarm_settings.sound_selected == "" and sound != ""): return if value == True: print("You selected " + sound) else: print("You deselected " + sound) alarm_settings.sound_source = "" alarm_settings.sound_selected = "" if self.ids["settings_menu_wake_up_sound_Birds"].active: alarm_settings.sound_selected = sound alarm_settings.sound_source = "Woodpecker Chirps - QuickSounds.com.mp3" elif self.ids["settings_menu_wake_up_sound_NpoRadio1"].active: alarm_settings.sound_selected = sound alarm_settings.sound_source = "https://icecast.omroep.nl/radio1-bb-mp3" def volume_slider_value(self, *args): alarm_settings = App.get_running_app().alarm_settings alarm_settings.volume = int(args[1]) print("Volume changed to " + str(alarm_settings.volume)) with pulsectl.Pulse('volume-increaser') as pulse: for sink in pulse.sink_list(): # Volume is usually in 0-1.0 range, with >1.0 being soft-boosted old_vol = pulse.volume_get_all_chans(sink) pulse.volume_set_all_chans(sink, alarm_settings.volume / 20.0) new_vol = pulse.volume_get_all_chans(sink) print("HW volume changed from " + str(old_vol) + " to " + str(new_vol)) def wake_up_brightness_slider_value(self, *args): alarm_settings = App.get_running_app().alarm_settings alarm_settings.wake_up_brightness = int(args[1]) print("Wake up brightness changed to " + str(alarm_settings.wake_up_brightness)) def reading_light_brightness_slider_value(self, *args): alarm_settings = App.get_running_app().alarm_settings alarm_settings.reading_light_brightness = int(args[1]) print("Reading light brightness changed to " + str(alarm_settings.reading_light_brightness)) def display_brightness_slider_value(self, *args): alarm_settings = App.get_running_app().alarm_settings alarm_settings.display_brightness = int(args[1]) print("Display brightness changed to " + str(alarm_settings.display_brightness)) def on_alarm_button_pressed(self): alarm_settings = App.get_running_app().alarm_settings alarm_settings.alarm_modified = False self.set_alarm_timeout_counter = 0 if self.view == "set_alarm": self.view = "clock" alarm_settings.alarm_activated = False else: self.view = "set_alarm" alarm_settings.alarm_activated = True def on_settings_button_pressed(self): print("settings button pressed from view " + self.view) if self.view != "settings_menu": self.view = "settings_menu" else: self.view = "clock" print("view updated to " + self.view) def on_touch_up(self, touch): self.grabbed = "" alarm_settings = App.get_running_app().alarm_settings if (self.view == "set_alarm") and (self.grabbed == "hour" or self.grabbed == "minute"): self.set_alarm_timeout_counter = 0 super(MyClockWidget, self).on_touch_up(touch) def on_touch_move(self, touch): alarm_settings = App.get_running_app().alarm_settings 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": alarm_settings.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 = alarm_settings.alarm_time.hour if alarm_settings.alarm_time.minute >= 55 and minute <= 5: hour += 1 elif alarm_settings.alarm_time.minute <= 5 and minute >= 55: hour -= 1 if hour == 24: hour = 0 elif hour == -1: hour = 23 alarm_settings.alarm_time = datetime.datetime(alarm_settings.alarm_time.year, \ alarm_settings.alarm_time.month, alarm_settings.alarm_time.day, \ hour, minute, alarm_settings.alarm_time.second, 0) elif self.grabbed == "hour": alarm_settings.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 alarm_settings.alarm_time.hour >= 12: hour += 12 # AM / PM correction if alarm_settings.alarm_time.hour == 11 and hour == 0: hour = 12 elif alarm_settings.alarm_time.hour == 23 and hour == 12: hour = 0 elif alarm_settings.alarm_time.hour == 0 and hour == 11: hour = 23 elif alarm_settings.alarm_time.hour == 12 and hour == 23: hour = 11 # AM / PM boundary alarm_settings.alarm_time = datetime.datetime(alarm_settings.alarm_time.year, \ alarm_settings.alarm_time.month, alarm_settings.alarm_time.day, \ hour, alarm_settings.alarm_time.minute, alarm_settings.alarm_time.second, 0) super(MyClockWidget, self).on_touch_move(touch) def on_touch_down(self, touch): global sound_process alarm_settings = App.get_running_app().alarm_settings time = alarm_settings.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 (0.85 <= touch.spos[0] <= 0.95) and (0.85 <= touch.spos[1] <= 0.95): self.on_settings_button_pressed() elif self.view == "set_alarm": 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" elif self.view == "settings_menu": pass elif self.view == "clock": if sound_process is not None: kill_sound_process() super(MyClockWidget, self).on_touch_down(touch) class MyApp(App): alarm_settings = AlarmSettings() update_rate = 60.0 # apply volume setting with pulsectl.Pulse('volume-increaser') as pulse: for sink in pulse.sink_list(): # Volume is usually in 0-1.0 range, with >1.0 being soft-boosted old_vol = pulse.volume_get_all_chans(sink) pulse.volume_set_all_chans(sink, alarm_settings.volume / 20.0) new_vol = pulse.volume_get_all_chans(sink) print("HW volume changed from " + str(old_vol) + " to " + str(new_vol)) def build(self): clock_widget = MyClockWidget() update_rate = App.get_running_app().update_rate # update initially, just after construction of the widget is complete Clock.schedule_once(clock_widget.update_display, 0) # then update at update_rate times per second Clock.schedule_interval(clock_widget.update_display, 1.0/update_rate) 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()