diff --git a/deploy-prod.sh b/deploy-prod.sh new file mode 100755 index 0000000..f2159ac --- /dev/null +++ b/deploy-prod.sh @@ -0,0 +1,242 @@ +#!/bin/bash +# IPMI Controller - Production Deployment Script +# Run this on the production server to set up auto-start and persistence + +set -e + +INSTALL_DIR="/opt/ipmi-controller" +DATA_DIR="$INSTALL_DIR/data" +SERVICE_NAME="ipmi-controller" +USER="devmatrix" + +echo "🌡️ IPMI Controller - Production Deployment" +echo "=============================================" +echo "" + +# Check if running as root +if [ "$EUID" -ne 0 ]; then + echo "❌ Please run as root: sudo ./deploy-prod.sh" + exit 1 +fi + +# Get the source directory +SOURCE_DIR="${1:-$(pwd)}" +if [ ! -f "$SOURCE_DIR/web_server.py" ]; then + echo "❌ Cannot find web_server.py in $SOURCE_DIR" + echo "Usage: sudo ./deploy-prod.sh [source-directory]" + exit 1 +fi + +echo "📁 Source: $SOURCE_DIR" +echo "📁 Install: $INSTALL_DIR" +echo "" + +# Create directories +echo "📂 Creating directories..." +mkdir -p "$INSTALL_DIR" +mkdir -p "$DATA_DIR" +mkdir -p /var/log/ipmi-controller + +# Copy application files +echo "📋 Copying application files..." +cp "$SOURCE_DIR/web_server.py" "$INSTALL_DIR/" +cp "$SOURCE_DIR/fan_controller.py" "$INSTALL_DIR/" +cp "$SOURCE_DIR/requirements.txt" "$INSTALL_DIR/" + +# Install dependencies +echo "🐍 Installing Python dependencies..." +pip3 install -q -r "$SOURCE_DIR/requirements.txt" || pip install -q -r "$SOURCE_DIR/requirements.txt" + +# Install ipmitool if not present +if ! command -v ipmitool &> /dev/null; then + echo "📦 Installing ipmitool..." + apt-get update -qq + apt-get install -y -qq ipmitool +fi + +# Preserve existing config if it exists +if [ -f "$DATA_DIR/config.json" ]; then + echo "💾 Preserving existing configuration..." + cp "$DATA_DIR/config.json" "$DATA_DIR/config.json.backup.$(date +%Y%m%d%H%M%S)" +else + echo "⚙️ Creating default configuration..." + cat > "$DATA_DIR/config.json" << 'EOF' +{ + "ipmi_host": "", + "ipmi_username": "", + "ipmi_password": "", + "ipmi_port": 623, + "http_sensor_enabled": false, + "http_sensor_url": "", + "http_sensor_timeout": 10, + "enabled": false, + "poll_interval": 10, + "min_speed": 10, + "max_speed": 100, + "panic_temp": 85, + "panic_speed": 100, + "panic_on_no_data": true, + "no_data_timeout": 60, + "primary_sensor": "cpu", + "sensor_preference": "auto", + "fans": {}, + "fan_groups": {}, + "fan_curves": { + "Balanced": { + "points": [ + {"temp": 30, "speed": 10}, + {"temp": 35, "speed": 12}, + {"temp": 40, "speed": 15}, + {"temp": 45, "speed": 20}, + {"temp": 50, "speed": 30}, + {"temp": 55, "speed": 40}, + {"temp": 60, "speed": 55}, + {"temp": 65, "speed": 70}, + {"temp": 70, "speed": 85}, + {"temp": 75, "speed": 95}, + {"temp": 80, "speed": 100} + ], + "sensor_source": "cpu", + "applies_to": "all" + }, + "Silent": { + "points": [ + {"temp": 30, "speed": 5}, + {"temp": 40, "speed": 10}, + {"temp": 50, "speed": 15}, + {"temp": 55, "speed": 25}, + {"temp": 60, "speed": 35}, + {"temp": 65, "speed": 50}, + {"temp": 70, "speed": 70}, + {"temp": 75, "speed": 85}, + {"temp": 80, "speed": 100} + ], + "sensor_source": "cpu", + "applies_to": "all" + }, + "Performance": { + "points": [ + {"temp": 30, "speed": 20}, + {"temp": 35, "speed": 25}, + {"temp": 40, "speed": 35}, + {"temp": 45, "speed": 45}, + {"temp": 50, "speed": 55}, + {"temp": 55, "speed": 70}, + {"temp": 60, "speed": 85}, + {"temp": 65, "speed": 95}, + {"temp": 70, "speed": 100} + ], + "sensor_source": "cpu", + "applies_to": "all" + } + }, + "active_curve": "Balanced", + "theme": "dark" +} +EOF +fi + +# Create users file if not exists +if [ ! -f "$DATA_DIR/users.json" ]; then + echo '{"users": {}}' > "$DATA_DIR/users.json" +fi + +# Set ownership +chown -R "$USER:$USER" "$INSTALL_DIR" +chown -R "$USER:$USER" /var/log/ipmi-controller + +# Create systemd service +echo "🔧 Creating systemd service..." +cat > "/etc/systemd/system/$SERVICE_NAME.service" << EOF +[Unit] +Description=IPMI Controller - Fan Control for Dell Servers +After=network.target +Wants=network.target + +[Service] +Type=simple +User=$USER +Group=$USER +WorkingDirectory=$INSTALL_DIR +Environment="PYTHONUNBUFFERED=1" +Environment="DATA_DIR=$DATA_DIR" +Environment="LOG_DIR=/var/log/ipmi-controller" +ExecStart=/usr/bin/python3 $INSTALL_DIR/web_server.py +ExecStop=/bin/kill -TERM \$MAINPID +ExecReload=/bin/kill -HUP \$MAINPID + +# Restart policy +Restart=always +RestartSec=10 +StartLimitInterval=60s +StartLimitBurst=3 + +# Logging +StandardOutput=append:/var/log/ipmi-controller/server.log +StandardError=append:/var/log/ipmi-controller/error.log + +# Security +NoNewPrivileges=false +ProtectSystem=no +ProtectHome=no + +[Install] +WantedBy=multi-user.target +EOF + +# Create logrotate config +echo "📝 Creating logrotate config..." +cat > "/etc/logrotate.d/ipmi-controller" << EOF +/var/log/ipmi-controller/*.log { + daily + rotate 14 + compress + delaycompress + missingok + notifempty + create 644 $USER $USER + sharedscripts + postrotate + systemctl reload ipmi-controller + endscript +} +EOF + +# Reload systemd +echo "🔄 Reloading systemd..." +systemctl daemon-reload + +# Enable service +echo "✅ Enabling service to start on boot..." +systemctl enable "$SERVICE_NAME" + +echo "" +echo "🚀 Starting IPMI Controller..." +systemctl start "$SERVICE_NAME" + +sleep 2 + +# Check status +if systemctl is-active --quiet "$SERVICE_NAME"; then + echo "" + echo "✅ IPMI Controller is running!" + echo "" + echo "🌐 Access: http://$(hostname -I | awk '{print $1}'):8000" + echo "" + echo "📋 Management Commands:" + echo " Status: sudo systemctl status $SERVICE_NAME" + echo " Logs: sudo journalctl -u $SERVICE_NAME -f" + echo " Restart: sudo systemctl restart $SERVICE_NAME" + echo " Stop: sudo systemctl stop $SERVICE_NAME" + echo "" + echo "💾 Settings: $DATA_DIR/config.json" + echo "📜 Logs: /var/log/ipmi-controller/" + echo "" + echo "✅ Production deployment complete!" + echo " Service will auto-start on boot." +else + echo "" + echo "⚠️ Service failed to start. Check logs:" + echo " sudo journalctl -u $SERVICE_NAME -f" + exit 1 +fi diff --git a/server.log b/server.log index b21388f..60b66f6 100644 --- a/server.log +++ b/server.log @@ -25,3 +25,321 @@ INFO: 192.168.5.30:61387 - "POST /api/config/ipmi HTTP/1.1" 200 OK INFO: 192.168.5.30:61387 - "GET /api/status HTTP/1.1" 200 OK INFO: 192.168.5.30:61387 - "GET /favicon.ico HTTP/1.1" 200 OK INFO: 192.168.5.30:61387 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:61387 - "GET /api/status HTTP/1.1" 200 OK +2026-02-20 17:07:56,586 - fan_controller - INFO - Saved config to /home/devmatrix/projects/fan-controller-v2/data/config.json +2026-02-20 17:07:56,769 - fan_controller - INFO - Manual fan control enabled +2026-02-20 17:07:56,924 - fan_controller - INFO - Connected to IPMI at 192.168.5.191 +2026-02-20 17:07:56,925 - fan_controller - INFO - HTTP sensor client initialized for http://192.168.5.200:8888 +2026-02-20 17:07:56,926 - fan_controller - INFO - IPMI Controller service started +INFO: 192.168.5.30:61387 - "POST /api/control/auto HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:61387 - "GET /api/status HTTP/1.1" 200 OK +2026-02-20 17:07:57,084 - fan_controller - INFO - Manual fan control enabled +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:61387 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +2026-02-20 17:08:01,333 - fan_controller - INFO - Saved config to /home/devmatrix/projects/fan-controller-v2/data/config.json +2026-02-20 17:08:01,540 - fan_controller - INFO - Fan 0xff speed set to 45% +INFO: 192.168.5.30:61387 - "POST /api/control/manual HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:57617 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:57617 - "GET /api/status HTTP/1.1" 200 OK +2026-02-20 17:08:03,050 - fan_controller - INFO - Fan 0xff speed set to 32% +2026-02-20 17:08:03,050 - fan_controller - INFO - All fans set to 32% (Temp 51.0°C) +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:57617 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:57617 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:57617 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:57617 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:57617 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:149: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + self._sessions[token] = (username, datetime.utcnow() + timedelta(days=7)) +INFO: 127.0.0.1:53770 - "POST /api/auth/login HTTP/1.1" 200 OK +INFO: 127.0.0.1:53786 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:46972 - "POST /api/auth/login HTTP/1.1" 200 OK +2026-02-20 17:10:19,582 - fan_controller - INFO - Saved config to /home/devmatrix/projects/fan-controller-v2/data/config.json +2026-02-20 17:10:19,744 - fan_controller - INFO - Manual fan control enabled +INFO: 127.0.0.1:46980 - "POST /api/control/auto HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:33742 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +2026-02-20 17:10:34,455 - fan_controller - INFO - Fan 0xff speed set to 30% +2026-02-20 17:10:34,456 - fan_controller - INFO - All fans set to 30% (Temp 50.0°C) +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +2026-02-20 17:11:07,821 - fan_controller - INFO - Fan 0xff speed set to 32% +2026-02-20 17:11:07,822 - fan_controller - INFO - All fans set to 32% (Temp 51.0°C) +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +2026-02-20 17:11:54,564 - fan_controller - INFO - Fan 0xff speed set to 30% +2026-02-20 17:11:54,564 - fan_controller - INFO - All fans set to 30% (Temp 50.0°C) +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +2026-02-20 17:12:24,621 - fan_controller - INFO - Fan 0xff speed set to 32% +2026-02-20 17:12:24,622 - fan_controller - INFO - All fans set to 32% (Temp 51.0°C) +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET / HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +2026-02-20 17:12:50,306 - fan_controller - INFO - Saved config to /home/devmatrix/projects/fan-controller-v2/data/config.json +INFO: 192.168.5.30:55959 - "POST /api/curves/active HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +2026-02-20 17:12:51,151 - fan_controller - INFO - Saved config to /home/devmatrix/projects/fan-controller-v2/data/config.json +INFO: 192.168.5.30:55959 - "POST /api/curves/active HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +2026-02-20 17:13:04,319 - fan_controller - INFO - Saved config to /home/devmatrix/projects/fan-controller-v2/data/config.json +2026-02-20 17:13:04,478 - fan_controller - INFO - Manual fan control enabled +INFO: 192.168.5.30:55959 - "POST /api/control/auto HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + if datetime.utcnow() > expiry: +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK +INFO: 192.168.5.30:55959 - "GET /api/status HTTP/1.1" 200 OK