From f4e6cdbeeafcfbc7a2fcedf7a76c4b21676b018f Mon Sep 17 00:00:00 2001 From: Aaro Varis Date: Thu, 19 Feb 2026 10:06:06 +0200 Subject: [PATCH] Add webserver functionality and HTML interface for alarm clock --- public/index.html | 113 ++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 3 +- src/main.py | 11 +++++ src/webserver.py | 117 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 public/index.html create mode 100644 src/webserver.py diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..d208eba --- /dev/null +++ b/public/index.html @@ -0,0 +1,113 @@ + + + + + + Alarm Clock + + + +
+

Alarm Clock

+
--:--:--
+
Alarm: --:--
+
+
+ + : + +
+ + +
+
+
+ + + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e985466..8f7925a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ astral -requests \ No newline at end of file +requests +flask \ No newline at end of file diff --git a/src/main.py b/src/main.py index 23e83b3..b351cc2 100644 --- a/src/main.py +++ b/src/main.py @@ -3,7 +3,9 @@ from .shelly import ShellyDevice from .audio import AudioPlayer from .alarm_clock import AlarmClock from .relay import Relay +from .webserver import run_server from os import path +import threading def main(): """Main entry point for the alarm clock application.""" @@ -47,6 +49,15 @@ def main(): dismiss_action=lambda: relay.off() ) + # Start webserver in background thread + server_thread = threading.Thread( + target=run_server, + kwargs={'alarm_clock': alarm_clock, 'host': '0.0.0.0', 'port': 5000}, + daemon=True + ) + server_thread.start() + print("Webserver started on http://0.0.0.0:5000") + alarm_clock.start() diff --git a/src/webserver.py b/src/webserver.py new file mode 100644 index 0000000..4aabc56 --- /dev/null +++ b/src/webserver.py @@ -0,0 +1,117 @@ +from flask import Flask, jsonify, request, send_from_directory +from datetime import datetime +from zoneinfo import ZoneInfo +from os import path + +from .config import CITY + + +def create_app(alarm_clock=None): + """Create and configure the Flask application.""" + + # Get the absolute path to the public folder + public_folder = path.abspath(path.join(path.dirname(__file__), '..', 'public')) + + app = Flask(__name__, static_folder=public_folder, static_url_path='') + + # Store alarm_clock reference + app.alarm_clock = alarm_clock + + # Serve static files from public folder + @app.route('/') + def serve_index(): + return send_from_directory(public_folder, 'index.html') + + @app.route('/') + def serve_static(filename): + return send_from_directory(public_folder, filename) + + # API endpoints + @app.route('/api/status', methods=['GET']) + def get_status(): + """Get current alarm clock status.""" + now = datetime.now(ZoneInfo(CITY.timezone)) + + response = { + 'current_time': now.strftime('%H:%M:%S'), + 'timezone': CITY.timezone, + 'alarm_active': False, + 'alarm_time': None, + 'config_mode': 'normal' + } + + if app.alarm_clock: + response['alarm_active'] = app.alarm_clock.alarm_active + response['alarm_time'] = app.alarm_clock.alarm_time.strftime('%H:%M') + response['config_mode'] = app.alarm_clock.config_mode + + return jsonify(response) + + @app.route('/api/alarm', methods=['GET']) + def get_alarm(): + """Get current alarm time.""" + if not app.alarm_clock: + return jsonify({'error': 'Alarm clock not initialized'}), 503 + + return jsonify({ + 'hour': app.alarm_clock.alarm_time.hour, + 'minute': app.alarm_clock.alarm_time.minute, + 'formatted': app.alarm_clock.alarm_time.strftime('%H:%M') + }) + + @app.route('/api/alarm', methods=['POST']) + def set_alarm(): + """Set alarm time. Expects JSON with 'hour' and 'minute'.""" + if not app.alarm_clock: + return jsonify({'error': 'Alarm clock not initialized'}), 503 + + data = request.get_json() + if not data: + return jsonify({'error': 'JSON body required'}), 400 + + hour = data.get('hour') + minute = data.get('minute') + + if hour is None or minute is None: + return jsonify({'error': 'Both hour and minute are required'}), 400 + + try: + hour = int(hour) + minute = int(minute) + except (ValueError, TypeError): + return jsonify({'error': 'Hour and minute must be integers'}), 400 + + if not (0 <= hour <= 23): + return jsonify({'error': 'Hour must be between 0 and 23'}), 400 + if not (0 <= minute <= 59): + return jsonify({'error': 'Minute must be between 0 and 59'}), 400 + + app.alarm_clock.alarm_time = app.alarm_clock.alarm_time.replace( + hour=hour, minute=minute + ) + app.alarm_clock.reset_wakeup_actions() + + return jsonify({ + 'success': True, + 'alarm_time': app.alarm_clock.alarm_time.strftime('%H:%M') + }) + + @app.route('/api/dismiss', methods=['POST']) + def dismiss_alarm(): + """Dismiss the active alarm.""" + if not app.alarm_clock: + return jsonify({'error': 'Alarm clock not initialized'}), 503 + + if not app.alarm_clock.alarm_active: + return jsonify({'message': 'No active alarm to dismiss'}), 200 + + app.alarm_clock.dismiss_alarm() + return jsonify({'success': True, 'message': 'Alarm dismissed'}) + + return app + + +def run_server(alarm_clock=None, host='0.0.0.0', port=5000, debug=False): + """Run the webserver.""" + app = create_app(alarm_clock) + app.run(host=host, port=port, debug=debug, threaded=True)