- 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
230 lines
9.3 KiB
Python
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)
|