Files
raspi-iot-ryhmatyo/src/alarm_clock.py
Aaro Varis ae064f059d 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
2026-02-18 16:21:42 +02:00

230 lines
9.3 KiB
Python

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)