Refactor alarm clock application structure and add core functionality

- Moved legacy entry point to init.py
- Created src/__init__.py for module initialization
- Implemented AlarmClock class in src/alarm_clock.py
- Added audio playback capabilities in src/audio.py
- Configured Shelly device interaction in src/shelly.py
- Introduced wakeup action handling in src/wakeup_action.py
- Added location utilities for sun times in src/location.py
- Established configuration settings in src/config.py
- Developed main application logic in src/main.py
This commit is contained in:
2026-02-18 16:21:42 +02:00
parent b2302dc94b
commit ae064f059d
9 changed files with 433 additions and 314 deletions

229
src/alarm_clock.py Normal file
View File

@@ -0,0 +1,229 @@
from time import time
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from typing import Callable
from grove.gpio import GPIO
from grove.display.jhd1802 import JHD1802
from .config import CITY, SHELLY_DEVICE_ID
from .shelly import set_shelly_plug_state
from .wakeup_action import WakeUpAction
class AlarmClock:
"""Main alarm clock controller with LCD display and GPIO controls."""
wakeup_actions: list[WakeUpAction] = []
led_gpio: GPIO
button_gpio: GPIO
relay_gpio: GPIO
last_known_button_state: bool = False
lcd: JHD1802
last_button_press_time: float = 0.0
last_button_state_change_time: float = 0.0
debounce_delay: float = 0.05 # 50ms debounce delay
long_press_threshold: float = 1.0 # 1 second for long press
button_press_start_time: float = 0.0
config_mode: str = "normal" # modes: normal, set_hour, set_minute
alarm_time: datetime = datetime.now(ZoneInfo("Europe/Helsinki")).replace(
hour=9, minute=59, second=0, microsecond=0
)
actions_reset_for_today: bool = False
alarm_active: bool = False
last_lcd_text: str = ""
def __init__(self, pins: dict = {}):
self.led_gpio = GPIO(pins.get("led", 5), GPIO.OUT)
self.button_gpio = GPIO(pins.get("button", 6), GPIO.IN)
self.relay_gpio = GPIO(pins.get("relay", 16), GPIO.OUT)
self.lcd = JHD1802()
self.wakeup_actions = []
self.add_wakeup_action(0, lambda: self.trigger_alarm()) # Activate alarm state
self.set_lcd_text("AlarmClock Init")
def add_wakeup_action(
self,
offset_seconds: int,
action: Callable[[], None],
dismiss_action: Callable[[], None] | None = None
):
"""Add a wakeup action to be triggered offset_seconds before alarm time."""
self.wakeup_actions.append(WakeUpAction(offset_seconds, action, dismiss_action))
def reset_wakeup_actions(self):
"""Reset all wakeup actions to untriggered state."""
for action in self.wakeup_actions:
action.triggered = False
self.actions_reset_for_today = True
print("Wakeup actions reset")
def trigger_alarm(self):
"""Activate the alarm state."""
if self.alarm_active:
return
self.alarm_active = True
print("Alarm is now active!")
def dismiss_alarm(self):
"""Dismiss the active alarm and call dismiss actions."""
if not self.alarm_active:
return
self.alarm_active = False
for action in self.wakeup_actions:
if action.dismiss_action:
action.dismiss_action()
print("Alarm dismissed")
def check_wakeup_actions(self):
"""Check and trigger any wakeup actions that are due."""
if not self.wakeup_actions:
return
# If not in normal mode, skip wakeup actions
if self.config_mode != "normal":
return
now = datetime.now(ZoneInfo(CITY.timezone))
today_alarm = now.replace(
hour=self.alarm_time.hour,
minute=self.alarm_time.minute,
second=0,
microsecond=0
)
# Find the earliest action (largest offset)
max_offset = max(action.offset_seconds for action in self.wakeup_actions)
first_trigger_time = today_alarm - timedelta(seconds=max_offset)
reset_time = first_trigger_time - timedelta(seconds=10)
alarm_window_end = today_alarm + timedelta(minutes=1)
# Reset actions 10 seconds before the first one should trigger
if now >= reset_time and now < first_trigger_time and not self.actions_reset_for_today:
self.reset_wakeup_actions()
# After alarm window ends, allow reset to happen again tomorrow
if now >= alarm_window_end:
self.actions_reset_for_today = False
for wakeup_action in self.wakeup_actions:
if wakeup_action.triggered:
continue
trigger_time = today_alarm - timedelta(seconds=wakeup_action.offset_seconds)
if now >= trigger_time and now < today_alarm + timedelta(minutes=1):
print(f"Triggering wakeup action (offset: {wakeup_action.offset_seconds}s)")
wakeup_action.action()
wakeup_action.triggered = True
def start(self):
"""Start the alarm clock main loop."""
print("AlarmClock started")
while True:
self.loop()
def set_relay_state(self, state: bool):
"""Set the relay GPIO state."""
self.relay_gpio.write(1 if state else 0)
def loop(self):
"""Main loop iteration - handles display and button input."""
current_button_state = not self.button_gpio.read()
current_time = time()
if self.config_mode == "normal":
now = datetime.now(ZoneInfo(CITY.timezone))
current_hour = now.hour
current_minute = now.minute
current_second = now.second
if self.alarm_active:
self.set_lcd_text("!! ALARM !!\nPress to dismiss")
else:
self.set_lcd_text(
f"Time {current_hour:02d}:{current_minute:02d}:{current_second:02d}\n"
f"Alarm {self.alarm_time.hour:02d}:{self.alarm_time.minute:02d}"
)
# Check and trigger wakeup actions
self.check_wakeup_actions()
# Only process button state change if debounce delay has passed
if current_button_state != self.last_known_button_state:
if (current_time - self.last_button_state_change_time) >= self.debounce_delay:
self.last_known_button_state = current_button_state
self.last_button_state_change_time = current_time
self.on_button_press("button", current_button_state)
# Check for long press while button is held
if current_button_state and self.button_press_start_time > 0:
press_duration = current_time - self.button_press_start_time
if press_duration >= self.long_press_threshold:
self.on_long_press()
self.button_press_start_time = 0.0 # Reset to prevent repeated triggers
def on_button_press(self, button: str, state: bool):
"""Handle button press/release events."""
print(f"Button '{button}' pressed state: {state}")
current_time = time()
if button == "button":
if state: # Button pressed down
self.button_press_start_time = current_time
else: # Button released
if self.button_press_start_time > 0:
press_duration = current_time - self.button_press_start_time
self.button_press_start_time = 0.0
# Only handle as short press if it wasn't a long press
if press_duration < self.long_press_threshold:
self.on_short_press()
def on_short_press(self):
"""Handle short button press based on current mode."""
if self.config_mode == "normal":
if self.alarm_active:
# Dismiss active alarm
self.dismiss_alarm()
else:
# Normal mode: toggle relay
self.set_relay_state(not self.relay_gpio.read())
set_shelly_plug_state(SHELLY_DEVICE_ID, self.relay_gpio.read() == 0)
elif self.config_mode == "set_hour":
# Increment hour
self.alarm_time = self.alarm_time.replace(hour=(self.alarm_time.hour + 1) % 24)
self.set_lcd_text(f"Set Hour: {self.alarm_time.hour:02d}")
elif self.config_mode == "set_minute":
# Increment minute
self.alarm_time = self.alarm_time.replace(minute=(self.alarm_time.minute + 1) % 60)
self.set_lcd_text(f"Set Min: {self.alarm_time.minute:02d}")
self.reset_wakeup_actions() # Reset wakeup actions to apply new alarm time immediately
def on_long_press(self):
"""Handle long button press - cycle through configuration modes."""
if self.config_mode == "normal":
self.config_mode = "set_hour"
self.set_lcd_text(f"Set Hour: {self.alarm_time.hour:02d}")
print("Entering hour setting mode")
elif self.config_mode == "set_hour":
self.config_mode = "set_minute"
self.set_lcd_text(f"Set Min: {self.alarm_time.minute:02d}")
print("Switching to minute setting mode")
elif self.config_mode == "set_minute":
self.config_mode = "normal"
self.set_lcd_text(f"Saved: {self.alarm_time.hour:02d}:{self.alarm_time.minute:02d}")
print(f"Alarm time saved: {self.alarm_time.hour:02d}:{self.alarm_time.minute:02d}")
def set_lcd_text(self, text: str):
"""Update the LCD display with new text."""
if text == self.last_lcd_text:
return
self.last_lcd_text = text
self.lcd.clear()
rows = text.split("\n")
for i, row in enumerate(rows):
if i == 0:
self.lcd.setCursor(0, 0)
self.lcd.write(f"{(' ' + row):<17}")
elif i == 1:
self.lcd.setCursor(1, 0)
self.lcd.write(row)