from time import time from astral import LocationInfo from astral.sun import sun from datetime import date, datetime, timedelta from zoneinfo import ZoneInfo from typing import Callable import requests import subprocess from grove.gpio import GPIO from grove.display.jhd1802 import JHD1802 shelly_server = "https://shelly-237-eu.shelly.cloud" shelly_token = "M2M5YTYxdWlkEFD82A2301693D62637E530B4CDDC0DC6E2CDD8F01BE5E5102000B92638A2DA74019E6A81E6D17E0" shelly_deviceId = "8cbfea9fd6d0" def setShellyPlugState(deviceId: str, state: bool) -> dict: """ Set the state of a Shelly plug/switch device. Args: deviceId: The Shelly device id state: True to turn on, False to turn off Returns: Response from the API as a dictionary """ url = f"{shelly_server}/v2/devices/api/set/switch" params = {"auth_key": shelly_token} payload = { "id": deviceId, "on": state } response = requests.post(url, params=params, json=payload) if response.status_code == 200: return {"success": True} else: return response.json() city = LocationInfo("Seinäjoki", "Finland", "Europe/Helsinki", 62.7900, 22.8400) s = sun(city.observer, date=date.today()) print(f"City: {city.name}, {city.region}") print(f"Timezone: {city.timezone}") print(f"Latitude: {city.latitude:.6f}; Longitude: {city.longitude:.6f}") def isSunUp() -> bool: now = datetime.now(city.tzinfo) return s["sunrise"] <= now <= s["sunset"] def playAudio(filePath: str) -> None: """ Play an audio file through the Raspberry Pi's standard aux output. Args: filePath: Path to the audio file (.wav, .mp3, .ogg, etc.) """ if filePath.endswith(".wav"): subprocess.Popen(["aplay", filePath], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) else: # Use mpg123 for mp3, or ffplay/mpv as fallback for other formats subprocess.Popen(["mpg123", "-q", filePath], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) class WakeUpAction: offsetSeconds: int # how many seconds before alarm time to trigger action: Callable[[], None] triggered: bool = False def __init__(self, offsetSeconds: int, action: Callable[[], None]): self.offsetSeconds = offsetSeconds self.action = action self.triggered = False # sunset and sunrise class AlarmClock: wakeupActions: list[WakeUpAction] = [] ledGpio: GPIO buttonGpio: GPIO relayGpio: GPIO lastKnownButtonState: bool = False lcd: JHD1802 lastButtonPressTime: float = 0.0 lastButtonStateChangeTime: float = 0.0 debounceDelay: float = 0.05 # 50ms debounce delay longPressThreshold: float = 1.0 # 1 second for long press buttonPressStartTime: float = 0.0 configMode: str = "normal" # modes: normal, set_hour, set_minute alarmTime: datetime = datetime.now(ZoneInfo("Europe/Helsinki")).replace(hour=9, minute=48, second=0, microsecond=0) actionsResetForToday: bool = False alarmActive: bool = False def __init__(self, pins: dict = {}): self.ledGpio = GPIO(pins.get("led", 5), GPIO.OUT) self.buttonGpio = GPIO(pins.get("button", 6), GPIO.IN) self.relayGpio = GPIO(pins.get("relay", 16), GPIO.OUT) self.lcd = JHD1802() self.wakeupActions = [] self.addWakeUpAction(0, lambda: self.triggerAlarm()) # Activate alarm state self.setLcdText("AlarmClock Init") def addWakeUpAction(self, offsetSeconds: int, action: Callable[[], None]): """Add a wakeup action to be triggered offsetSeconds before alarm time""" self.wakeupActions.append(WakeUpAction(offsetSeconds, action)) def resetWakeUpActions(self): for action in self.wakeupActions: action.triggered = False self.actionsResetForToday = True print("Wakeup actions reset") def triggerAlarm(self): if self.alarmActive: return self.alarmActive = True print("Alarm is now active!") def dismissAlarm(self): if not self.alarmActive: return self.alarmActive = False print("Alarm dismissed") def checkWakeUpActions(self): """Check and trigger any wakeup actions that are due""" if not self.wakeupActions: return now = datetime.now(ZoneInfo(city.timezone)) today_alarm = now.replace( hour=self.alarmTime.hour, minute=self.alarmTime.minute, second=0, microsecond=0 ) # Find the earliest action (largest offset) max_offset = max(action.offsetSeconds for action in self.wakeupActions) 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.actionsResetForToday: self.resetWakeUpActions() # After alarm window ends, allow reset to happen again tomorrow if now >= alarm_window_end: self.actionsResetForToday = False for wakeupAction in self.wakeupActions: if wakeupAction.triggered: continue trigger_time = today_alarm - timedelta(seconds=wakeupAction.offsetSeconds) if now >= trigger_time and now < today_alarm + timedelta(minutes=1): print(f"Triggering wakeup action (offset: {wakeupAction.offsetSeconds}s)") wakeupAction.action() wakeupAction.triggered = True def start(self): print("AlarmClock started") while True: self.loop() def setRelayState(self, state: bool): self.relayGpio.write(1 if state else 0) def loop(self): currentButtonState = not self.buttonGpio.read() currentTime = time() if (self.configMode == "normal"): currentMinute = datetime.now(ZoneInfo(city.timezone)).minute currentHour = datetime.now(ZoneInfo(city.timezone)).hour currrentSecond = datetime.now(ZoneInfo(city.timezone)).second if self.alarmActive: self.setLcdText(f"!! ALARM !!\nPress to dismiss") else: self.setLcdText(f"Time {currentHour:02d}:{currentMinute:02d}:{currrentSecond:02d}\nAlarm {self.alarmTime.hour:02d}:{self.alarmTime.minute:02d}") # Check and trigger wakeup actions self.checkWakeUpActions() # Only process button state change if debounce delay has passed if currentButtonState != self.lastKnownButtonState: if (currentTime - self.lastButtonStateChangeTime) >= self.debounceDelay: self.lastKnownButtonState = currentButtonState self.lastButtonStateChangeTime = currentTime self.onButtonPress("button", currentButtonState) # Check for long press while button is held if currentButtonState and self.buttonPressStartTime > 0: pressDuration = currentTime - self.buttonPressStartTime if pressDuration >= self.longPressThreshold: self.onLongPress() self.buttonPressStartTime = 0.0 # Reset to prevent repeated triggers def onButtonPress(self, button: str, state: bool): print(f"Button '{button}' pressed state: {state}") currentTime = time() if button == "button": if state: # Button pressed down self.buttonPressStartTime = currentTime else: # Button released if self.buttonPressStartTime > 0: pressDuration = currentTime - self.buttonPressStartTime self.buttonPressStartTime = 0.0 # Only handle as short press if it wasn't a long press if pressDuration < self.longPressThreshold: self.onShortPress() def onShortPress(self): """Handle short button press based on current mode""" if self.configMode == "normal": if self.alarmActive: # Dismiss active alarm self.dismissAlarm() else: # Normal mode: toggle relay self.setRelayState(not self.relayGpio.read()) setShellyPlugState(shelly_deviceId, self.relayGpio.read() == 0) elif self.configMode == "set_hour": # Increment hour self.alarmTime = self.alarmTime.replace(hour=(self.alarmTime.hour + 1) % 24) self.setLcdText(f"Set Hour: {self.alarmTime.hour:02d}") elif self.configMode == "set_minute": # Increment minute self.alarmTime = self.alarmTime.replace(minute=(self.alarmTime.minute + 1) % 60) self.setLcdText(f"Set Min: {self.alarmTime.minute:02d}") def onLongPress(self): """Handle long button press - cycle through configuration modes""" if self.configMode == "normal": self.configMode = "set_hour" self.setLcdText(f"Set Hour: {self.alarmTime.hour:02d}") print(f"Entering hour setting mode") elif self.configMode == "set_hour": self.configMode = "set_minute" self.setLcdText(f"Set Min: {self.alarmTime.minute:02d}") print(f"Switching to minute setting mode") elif self.configMode == "set_minute": self.configMode = "normal" self.setLcdText(f"Saved: {self.alarmTime.hour:02d}:{self.alarmTime.minute:02d}") print(f"Alarm time saved: {self.alarmTime.hour:02d}:{self.alarmTime.minute:02d}") lastLcdText: str = "" def setLcdText(self, text: str): if text == self.lastLcdText: return self.lastLcdText = 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) #print(f"LCD: {text}") pins = { "button": 6, "led": 5, "buzzer": 12, } alarm_clock = AlarmClock(pins) # Add wakeup actions (offsetSeconds = seconds before alarm to trigger) alarm_clock.addWakeUpAction(120, lambda: print("2 minutes to alarm!")) # 2 minutes before alarm alarm_clock.addWakeUpAction(90, lambda: setShellyPlugState(shelly_deviceId, True)) # activate plug 90s before alarm alarm_clock.addWakeUpAction(30, lambda: print("30 seconds to alarm!")) # 30 seconds before alarm alarm_clock.addWakeUpAction(0, lambda: print("Alarm triggered!")) # At alarm time alarm_clock.addWakeUpAction(0, lambda: playAudio("./boxing_bell_multiple.wav")) # Play sound at alarm time alarm_clock.start()