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:
229
src/alarm_clock.py
Normal file
229
src/alarm_clock.py
Normal 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)
|
||||
Reference in New Issue
Block a user