Add comprehensive documentation, README, sensor server script, and systemd service
This commit is contained in:
parent
96abc9bff9
commit
8cf9957518
472
README.md
472
README.md
|
|
@ -1,156 +1,406 @@
|
||||||
# IPMI Controller
|
# IPMI Controller
|
||||||
|
|
||||||
Advanced IPMI fan control for Dell servers with web interface, HTTP sensor support, multiple fan curves, and persistent configuration.
|
Advanced web-based fan control for Dell servers with IPMI support. Automatically adjust fan speeds based on temperature readings from IPMI sensors and optional HTTP lm-sensors endpoint.
|
||||||
|
|
||||||
|
**Version:** 1.0.0
|
||||||
|
**Author:** ImpulsiveFPS
|
||||||
|
**License:** MIT
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- 🌡️ **Dual Sensor Support** - IPMI + HTTP (lm-sensors from Proxmox/host)
|
- 🌡️ **Temperature Monitoring** - Real-time CPU, inlet, exhaust, and PCIe temperature monitoring
|
||||||
- 🌬️ **Smart Fan Control** - Automatic curves, manual control, panic mode
|
- 🌀 **Automatic Fan Control** - Dynamic fan speed adjustment based on customizable temperature curves
|
||||||
- 📊 **3 Preset Curves** - Balanced (default), Silent, Performance
|
- 📊 **Fan Groups** - Group fans together for unified control
|
||||||
- 👥 **Fan Groups** - Organize and control fans individually or in groups
|
- 📈 **Custom Curves** - Create custom fan curves and assign them to specific fan groups
|
||||||
- 🔍 **Fan Identify** - Visual fan identification
|
- 🖥️ **HTTP Sensors** - Optional integration with lm-sensors for additional temperature data
|
||||||
- 🎨 **Themes** - Dark and Light mode
|
- 🎨 **Dark/Light Theme** - Choose your preferred visual style
|
||||||
- 📱 **Responsive Web UI** - Works on desktop and mobile
|
- 🔒 **Secure** - Built-in authentication and session management
|
||||||
- 🔌 **Public API** - For external integrations
|
- 🚀 **Auto-Start** - Automatically resumes operation after system restart
|
||||||
- 💾 **Persistent Settings** - Survives restarts and updates
|
|
||||||
|
|
||||||
## Quick Start
|
---
|
||||||
|
|
||||||
### Automated Install (Recommended)
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Prerequisites](#prerequisites)
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [IPMI Setup](#ipmi-setup)
|
||||||
|
- [HTTP Sensors Setup (Optional)](#http-sensors-setup-optional)
|
||||||
|
- [First Run](#first-run)
|
||||||
|
- [Configuration](#configuration)
|
||||||
|
- [Troubleshooting](#troubleshooting)
|
||||||
|
- [Support](#support)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
### Hardware Requirements
|
||||||
|
|
||||||
|
- Dell server with IPMI support (iDRAC)
|
||||||
|
- Network connectivity to the server's IPMI interface
|
||||||
|
- A machine to run the IPMI Controller (can be the same server or a separate management host)
|
||||||
|
|
||||||
|
### Software Requirements
|
||||||
|
|
||||||
|
- Python 3.8+
|
||||||
|
- `ipmitool` (for IPMI communication)
|
||||||
|
- Linux-based system (tested on Ubuntu/Debian)
|
||||||
|
|
||||||
|
### Installing ipmitool
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/yourusername/ipmi-controller.git
|
# Ubuntu/Debian
|
||||||
cd ipmi-controller
|
sudo apt update
|
||||||
chmod +x install.sh
|
sudo apt install ipmitool
|
||||||
sudo ./install.sh
|
|
||||||
|
# Verify installation
|
||||||
|
ipmitool -V
|
||||||
```
|
```
|
||||||
|
|
||||||
This will:
|
---
|
||||||
- Install all dependencies
|
|
||||||
- Create systemd service for auto-start
|
|
||||||
- Set up persistent data directory
|
|
||||||
- Start the controller on boot
|
|
||||||
|
|
||||||
### Manual Install
|
## Installation
|
||||||
|
|
||||||
|
### 1. Clone the Repository
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install dependencies
|
git clone https://github.com/ImpulsiveFPS/IPMI-Controller.git
|
||||||
sudo apt-get install -y ipmitool python3-pip
|
cd IPMI-Controller
|
||||||
pip3 install -r requirements.txt
|
```
|
||||||
|
|
||||||
# Run
|
### 2. Install Python Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Required packages:
|
||||||
|
- fastapi
|
||||||
|
- uvicorn
|
||||||
|
- pydantic
|
||||||
|
- requests
|
||||||
|
|
||||||
|
### 3. Start the Application
|
||||||
|
|
||||||
|
```bash
|
||||||
python3 web_server.py
|
python3 web_server.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Access at `http://your-server:8000`
|
The web interface will be available at `http://localhost:8000`
|
||||||
|
|
||||||
## Initial Setup
|
### 4. (Optional) Systemd Service
|
||||||
|
|
||||||
1. Complete the setup wizard (create admin + IPMI config)
|
To run the controller as a system service:
|
||||||
2. Login with your admin credentials
|
|
||||||
3. (Optional) Set up HTTP sensor on your Proxmox host:
|
|
||||||
```bash
|
|
||||||
# On Proxmox server
|
|
||||||
curl -O https://raw.githubusercontent.com/yourusername/ipmi-controller/main/setup-sensors-server.sh
|
|
||||||
sudo ./setup-sensors-server.sh
|
|
||||||
```
|
|
||||||
4. Enable auto control and enjoy automatic fan management!
|
|
||||||
|
|
||||||
## Persistence
|
|
||||||
|
|
||||||
All your settings are automatically saved to `data/config.json`:
|
|
||||||
|
|
||||||
✅ IPMI configuration
|
|
||||||
✅ HTTP sensor settings
|
|
||||||
✅ Fan curves (Balanced, Silent, Performance)
|
|
||||||
✅ User accounts
|
|
||||||
✅ Theme preference
|
|
||||||
✅ All control settings
|
|
||||||
|
|
||||||
**Backups:**
|
|
||||||
```bash
|
|
||||||
./backup.sh backup # Create backup
|
|
||||||
./backup.sh list # List backups
|
|
||||||
./backup.sh restore [file] # Restore from backup
|
|
||||||
```
|
|
||||||
|
|
||||||
**Auto-backup via cron:**
|
|
||||||
```bash
|
|
||||||
# Add to crontab (keeps 30 days of backups)
|
|
||||||
0 2 * * * /opt/ipmi-controller/backup.sh auto
|
|
||||||
```
|
|
||||||
|
|
||||||
## Updating
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ipmi-controller
|
sudo cp ipmi-controller.service /etc/systemd/system/
|
||||||
git pull
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable ipmi-controller
|
||||||
# Settings are preserved automatically
|
sudo systemctl start ipmi-controller
|
||||||
sudo systemctl restart ipmi-controller
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Fan Curves
|
---
|
||||||
|
|
||||||
**Balanced** (Default) - Best for most users:
|
## IPMI Setup
|
||||||
```
|
|
||||||
30°C → 10% | 40°C → 15% | 50°C → 30% | 60°C → 55% | 70°C → 85% | 80°C → 100%
|
|
||||||
```
|
|
||||||
|
|
||||||
**Silent** - Noise-sensitive environments:
|
### Step 1: Configure iDRAC/IPMI Network Settings
|
||||||
```
|
|
||||||
30°C → 5% | 40°C → 10% | 50°C → 15% | 60°C → 35% | 70°C → 70% | 80°C → 100%
|
|
||||||
```
|
|
||||||
|
|
||||||
**Performance** - Heavy workloads:
|
1. Boot into your Dell server's BIOS/F2 setup
|
||||||
```
|
2. Navigate to **iDRAC Settings** → **Network**
|
||||||
30°C → 20% | 40°C → 35% | 50°C → 55% | 60°C → 85% | 70°C → 100%
|
3. Configure a static IP address for iDRAC (e.g., `192.168.5.191`)
|
||||||
```
|
4. Save and exit
|
||||||
|
|
||||||
## Documentation
|
Alternatively, configure via iDRAC web interface:
|
||||||
|
1. Access iDRAC at its current IP
|
||||||
|
2. Go to **iDRAC Settings** → **Network** → **IPV4 Settings**
|
||||||
|
3. Set static IP, subnet mask, and gateway
|
||||||
|
4. Apply changes
|
||||||
|
|
||||||
- [Setup Guide](SETUP.md) - Full installation instructions
|
### Step 2: Create IPMI User
|
||||||
- [Persistence Guide](PERSISTENCE.md) - Backup and restore
|
|
||||||
- [API Reference](API.md) - Public API documentation
|
|
||||||
|
|
||||||
## Docker
|
#### Method 1: Via iDRAC Web Interface (Recommended)
|
||||||
|
|
||||||
|
1. Log into iDRAC web interface
|
||||||
|
2. Go to **iDRAC Settings** → **User Authentication** → **Local Users**
|
||||||
|
3. Click **Add User** or edit an existing user
|
||||||
|
4. Configure:
|
||||||
|
- **User Name:** `root` (or your preferred username)
|
||||||
|
- **Password:** Strong password
|
||||||
|
- **IPMI LAN Privilege:** Administrator
|
||||||
|
- **Enable IPMI over LAN:** ✓ Checked
|
||||||
|
5. Save changes
|
||||||
|
|
||||||
|
#### Method 2: Via ipmitool (Local Access Required)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose up -d
|
# List current users
|
||||||
|
sudo ipmitool user list 1
|
||||||
|
|
||||||
|
# Create new user (ID 3)
|
||||||
|
sudo ipmitool user set name 3 root
|
||||||
|
sudo ipmitool user set password 3 YOUR_PASSWORD
|
||||||
|
sudo ipmitool channel setaccess 1 3 callin=on ipmi=on link=on privilege=4
|
||||||
|
sudo ipmitool user enable 3
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
sudo ipmitool user list 1
|
||||||
```
|
```
|
||||||
|
|
||||||
Data persists in `./data` directory.
|
### Step 3: Enable IPMI over LAN
|
||||||
|
|
||||||
## Management Commands
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Status
|
# Enable IPMI over LAN
|
||||||
sudo systemctl status ipmi-controller
|
sudo ipmitool lan set 1 ipsrc static
|
||||||
|
sudo ipmitool lan set 1 ipaddr 192.168.5.191
|
||||||
|
sudo ipmitool lan set 1 netmask 255.255.255.0
|
||||||
|
sudo ipmitool lan set 1 defgw ipaddr 192.168.5.1
|
||||||
|
sudo ipmitool lan set 1 access on
|
||||||
|
|
||||||
# Logs
|
# Verify settings
|
||||||
sudo journalctl -u ipmi-controller -f
|
sudo ipmitool lan print 1
|
||||||
|
|
||||||
# Restart
|
|
||||||
sudo systemctl restart ipmi-controller
|
|
||||||
|
|
||||||
# Stop
|
|
||||||
sudo systemctl stop ipmi-controller
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Step 4: Test IPMI Connection
|
||||||
|
|
||||||
|
From another machine on the network:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ipmitool -I lanplus -H 192.168.5.191 -U root -P YOUR_PASSWORD chassis status
|
||||||
|
```
|
||||||
|
|
||||||
|
If successful, you'll see server power status and other information.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## HTTP Sensors Setup (Optional)
|
||||||
|
|
||||||
|
HTTP sensors allow you to integrate additional temperature readings from lm-sensors running on your Proxmox host or other systems. This provides more granular CPU core temperatures.
|
||||||
|
|
||||||
|
### Step 1: Install lm-sensors on Remote Host
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On Proxmox or target host
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install lm-sensors
|
||||||
|
|
||||||
|
# Detect sensors
|
||||||
|
sudo sensors-detect
|
||||||
|
|
||||||
|
# Test
|
||||||
|
sensors
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Create HTTP Sensor Server Script
|
||||||
|
|
||||||
|
Create `sensor-server.py` on the remote host:
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Simple HTTP server for lm-sensors data"""
|
||||||
|
|
||||||
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
class SensorHandler(BaseHTTPRequestHandler):
|
||||||
|
def do_GET(self):
|
||||||
|
if self.path == '/sensors':
|
||||||
|
try:
|
||||||
|
result = subprocess.run(['sensors', '-j'],
|
||||||
|
capture_output=True, text=True)
|
||||||
|
data = json.loads(result.stdout)
|
||||||
|
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-Type', 'application/json')
|
||||||
|
self.send_header('Access-Control-Allow-Origin', '*')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps(data).encode())
|
||||||
|
except Exception as e:
|
||||||
|
self.send_response(500)
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps({'error': str(e)}).encode())
|
||||||
|
else:
|
||||||
|
self.send_response(404)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
pass # Suppress logs
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
server = HTTPServer(('0.0.0.0', 8888), SensorHandler)
|
||||||
|
print("Sensor server running on port 8888")
|
||||||
|
server.serve_forever()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Run Sensor Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 sensor-server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Or create a systemd service:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# /etc/systemd/system/sensor-server.service
|
||||||
|
[Unit]
|
||||||
|
Description=LM Sensors HTTP Server
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/bin/python3 /path/to/sensor-server.py
|
||||||
|
Restart=always
|
||||||
|
User=root
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl enable sensor-server
|
||||||
|
sudo systemctl start sensor-server
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Test HTTP Endpoint
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://192.168.5.200:8888/sensors
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see JSON output with sensor readings.
|
||||||
|
|
||||||
|
### Step 5: Configure in IPMI Controller
|
||||||
|
|
||||||
|
1. During setup wizard (Step 3), check "Enable HTTP Sensor"
|
||||||
|
2. Enter URL: `http://192.168.5.200:8888/sensors`
|
||||||
|
3. Click "Test HTTP Sensor" to verify connection
|
||||||
|
4. Complete setup
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## First Run
|
||||||
|
|
||||||
|
### Initial Setup Wizard
|
||||||
|
|
||||||
|
1. **Step 1: Create Admin Account**
|
||||||
|
- Username: Choose your admin username
|
||||||
|
- Password: Minimum 6 characters
|
||||||
|
- Confirm password
|
||||||
|
|
||||||
|
2. **Step 2: IPMI Configuration**
|
||||||
|
- Host/IP: Your iDRAC IP (e.g., `192.168.5.191`)
|
||||||
|
- Port: Usually `623`
|
||||||
|
- Username: IPMI username (e.g., `root`)
|
||||||
|
- Password: IPMI password
|
||||||
|
- Click "Test Connection" to verify
|
||||||
|
|
||||||
|
3. **Step 3: HTTP Sensor (Optional)**
|
||||||
|
- Enable HTTP Sensor: Check if using lm-sensors
|
||||||
|
- URL: `http://your-server:8888/sensors`
|
||||||
|
- Click "Test HTTP Sensor" to verify
|
||||||
|
|
||||||
|
4. **Enable Auto Fan Control**
|
||||||
|
- Check "Enable Auto Fan Control" to start automatic control immediately
|
||||||
|
- This activates the default "Balanced" fan curve
|
||||||
|
|
||||||
|
5. **Complete Setup**
|
||||||
|
- Click "Complete Setup" to finish
|
||||||
|
- You'll be logged in automatically
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Fan Curves
|
||||||
|
|
||||||
|
Fan curves define how fan speed responds to temperature:
|
||||||
|
|
||||||
|
1. Go to **Curves** tab
|
||||||
|
2. Click **+ Add Curve**
|
||||||
|
3. Configure:
|
||||||
|
- **Curve Name:** e.g., "Silent", "Performance"
|
||||||
|
- **Group:** (Optional) Assign to specific fan group
|
||||||
|
- **Points:** Add temperature → speed mappings
|
||||||
|
- Example: `30°C → 20%`, `50°C → 50%`, `70°C → 100%`
|
||||||
|
4. Click **Save Curve**
|
||||||
|
5. Click **Activate** to apply the curve
|
||||||
|
|
||||||
|
### Fan Groups
|
||||||
|
|
||||||
|
Groups allow unified control of multiple fans:
|
||||||
|
|
||||||
|
1. Go to **Fan Groups** tab
|
||||||
|
2. Click **+ Create First Group**
|
||||||
|
3. Enter group name
|
||||||
|
4. Select fans to include
|
||||||
|
5. Click **Save Group**
|
||||||
|
6. Use **Set Speed** to control all fans in group
|
||||||
|
|
||||||
|
### Quick Controls
|
||||||
|
|
||||||
|
- **Start Auto:** Enable automatic fan control
|
||||||
|
- **Stop Auto:** Return to manual/BIOS control
|
||||||
|
- **Manual Speed Slider:** Set all fans to specific speed
|
||||||
|
- **Identify Fan:** Flash individual fan to 100% for identification
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
**IPMI Connection Failed:**
|
### Connection Issues
|
||||||
- Verify IPMI IP, username, password
|
|
||||||
- Test: `ipmitool -I lanplus -H <ip> -U <user> -P <pass> mc info`
|
|
||||||
|
|
||||||
**No Temperature Data:**
|
**"Not connected" status:**
|
||||||
- Check HTTP sensor: `curl http://proxmox-ip:8888`
|
- Verify IPMI IP address is correct
|
||||||
- Verify `sensor_preference` is set to "auto" or "http"
|
- Check network connectivity: `ping 192.168.5.191`
|
||||||
|
- Test with ipmitool: `ipmitool -I lanplus -H 192.168.5.191 -U root chassis status`
|
||||||
|
- Ensure IPMI user has Administrator privileges
|
||||||
|
- Verify IPMI over LAN is enabled
|
||||||
|
|
||||||
**Settings Lost After Update:**
|
**"Connection timeout":**
|
||||||
- Ensure `data/` directory is not deleted
|
- Check firewall rules on IPMI network
|
||||||
- Check file permissions: `ls -la data/`
|
- Verify port 623 is open
|
||||||
|
- Try increasing timeout in settings
|
||||||
|
|
||||||
## License
|
### Fan Control Not Working
|
||||||
|
|
||||||
MIT License
|
**Fans not responding to speed changes:**
|
||||||
|
- Some Dell servers require manual fan control to be enabled first
|
||||||
|
- Check IPMI logs for errors
|
||||||
|
- Verify fan IDs are correct
|
||||||
|
- Try using individual fan controls to test
|
||||||
|
|
||||||
|
**"Manual fan control not supported":**
|
||||||
|
- Some server models don't support external fan control
|
||||||
|
- Check Dell documentation for your specific model
|
||||||
|
- Try updating iDRAC firmware
|
||||||
|
|
||||||
|
### HTTP Sensor Issues
|
||||||
|
|
||||||
|
**"HTTP Sensor not working":**
|
||||||
|
- Verify sensor server is running: `curl http://ip:8888/sensors`
|
||||||
|
- Check firewall on sensor host
|
||||||
|
- Ensure lm-sensors is properly configured
|
||||||
|
- Verify URL format includes `http://` prefix
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
- 🐛 **Report Bugs:** https://github.com/ImpulsiveFPS/IPMI-Controller/issues
|
||||||
|
- 📁 **GitHub Repo:** https://github.com/ImpulsiveFPS/IPMI-Controller
|
||||||
|
- ☕ **Support on Ko-fi:** https://ko-fi.com/impulsivefps
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acknowledgments
|
||||||
|
|
||||||
|
Built with:
|
||||||
|
- [FastAPI](https://fastapi.tiangolo.com/) - Web framework
|
||||||
|
- [Heroicons](https://heroicons.com/) - Icons
|
||||||
|
- [Lucide](https://lucide.dev/) - Additional icons
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**IPMI Controller v1.0.0 - Built by ImpulsiveFPS**
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
[Unit]
|
||||||
|
Description=IPMI Controller - Advanced Fan Control
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/opt/ipmi-controller
|
||||||
|
ExecStart=/usr/bin/python3 /opt/ipmi-controller/web_server.py
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
fastapi==0.109.0
|
fastapi>=0.100.0
|
||||||
uvicorn[standard]==0.27.0
|
uvicorn[standard]>=0.23.0
|
||||||
pydantic==2.5.3
|
pydantic>=2.0.0
|
||||||
pydantic-settings==2.1.0
|
requests>=2.31.0
|
||||||
python-multipart==0.0.6
|
|
||||||
paramiko==3.4.0
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Simple HTTP server for lm-sensors data
|
||||||
|
Run this on your Proxmox host or server with lm-sensors installed
|
||||||
|
"""
|
||||||
|
|
||||||
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
class SensorHandler(BaseHTTPRequestHandler):
|
||||||
|
def do_GET(self):
|
||||||
|
if self.path == '/sensors':
|
||||||
|
try:
|
||||||
|
result = subprocess.run(['sensors', '-j'],
|
||||||
|
capture_output=True, text=True)
|
||||||
|
data = json.loads(result.stdout)
|
||||||
|
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-Type', 'application/json')
|
||||||
|
self.send_header('Access-Control-Allow-Origin', '*')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps(data).encode())
|
||||||
|
except Exception as e:
|
||||||
|
self.send_response(500)
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps({'error': str(e)}).encode())
|
||||||
|
else:
|
||||||
|
self.send_response(404)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
pass # Suppress logs
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8888
|
||||||
|
server = HTTPServer(('0.0.0.0', port), SensorHandler)
|
||||||
|
print(f"Sensor server running on http://0.0.0.0:{port}/sensors")
|
||||||
|
print("Press Ctrl+C to stop")
|
||||||
|
try:
|
||||||
|
server.serve_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nShutting down...")
|
||||||
|
server.shutdown()
|
||||||
208
server.log
208
server.log
|
|
@ -1,84 +1,134 @@
|
||||||
INFO: Started server process [102941]
|
INFO: Started server process [103738]
|
||||||
INFO: Waiting for application startup.
|
INFO: Waiting for application startup.
|
||||||
2026-02-20 22:26:01,858 - fan_controller - INFO - Loaded config from /home/devmatrix/projects/fan-controller-v2/data/config.json
|
2026-02-20 22:29:23,172 - fan_controller - INFO - Loaded config from /home/devmatrix/projects/fan-controller-v2/data/config.json
|
||||||
2026-02-20 22:26:01,859 - __main__ - INFO - Auto-starting fan control (enabled in config)
|
2026-02-20 22:29:23,173 - __main__ - INFO - Auto-starting fan control (enabled in config)
|
||||||
2026-02-20 22:26:02,047 - fan_controller - INFO - Connected to IPMI at 192.168.5.191
|
2026-02-20 22:29:23,328 - fan_controller - INFO - Connected to IPMI at 192.168.5.191
|
||||||
2026-02-20 22:26:02,048 - fan_controller - INFO - HTTP sensor client initialized for http://192.168.5.200:8888
|
2026-02-20 22:29:23,329 - fan_controller - INFO - HTTP sensor client initialized for http://192.168.5.200:8888
|
||||||
2026-02-20 22:26:02,049 - fan_controller - INFO - IPMI Controller service started
|
2026-02-20 22:29:23,330 - fan_controller - INFO - IPMI Controller service started
|
||||||
INFO: Application startup complete.
|
INFO: Application startup complete.
|
||||||
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
|
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
|
||||||
2026-02-20 22:26:02,254 - fan_controller - INFO - Manual fan control enabled
|
2026-02-20 22:29:23,495 - fan_controller - INFO - Manual fan control enabled
|
||||||
INFO: 192.168.5.30:57456 - "GET /api/status HTTP/1.1" 401 Unauthorized
|
INFO: 192.168.5.30:54938 - "GET /api/status HTTP/1.1" 401 Unauthorized
|
||||||
INFO: 192.168.5.30:57456 - "GET /login HTTP/1.1" 200 OK
|
INFO: 192.168.5.30:54938 - "GET /login HTTP/1.1" 200 OK
|
||||||
2026-02-20 22:26:09,026 - fan_controller - INFO - Fan 0xff speed set to 14%
|
2026-02-20 22:29:29,168 - fan_controller - INFO - Fan 0xff speed set to 14%
|
||||||
2026-02-20 22:26:09,026 - fan_controller - INFO - All fans set to 14% (Temp 38.0°C)
|
2026-02-20 22:29:29,168 - fan_controller - INFO - All fans set to 14% (Temp 38.0°C)
|
||||||
INFO: 192.168.5.30:55757 - "GET /login HTTP/1.1" 200 OK
|
|
||||||
INFO: 192.168.5.30:55757 - "GET /favicon.ico 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).
|
/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).
|
||||||
self._sessions[token] = (username, datetime.utcnow() + timedelta(days=7))
|
self._sessions[token] = (username, datetime.utcnow() + timedelta(days=7))
|
||||||
INFO: 192.168.5.30:55757 - "POST /api/auth/login HTTP/1.1" 200 OK
|
INFO: 192.168.5.30:50571 - "POST /api/auth/login HTTP/1.1" 200 OK
|
||||||
INFO: 192.168.5.30:55757 - "GET / HTTP/1.1" 200 OK
|
INFO: 192.168.5.30:50571 - "GET / HTTP/1.1" 500 Internal Server Error
|
||||||
/home/devmatrix/projects/fan-controller-v2/web_server.py:163: 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).
|
ERROR: Exception in ASGI application
|
||||||
if datetime.utcnow() > expiry:
|
Traceback (most recent call last):
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/uvicorn/protocols/http/httptools_impl.py", line 419, in run_asgi
|
||||||
INFO: 192.168.5.30:55757 - "GET /favicon.ico HTTP/1.1" 200 OK
|
result = await app( # type: ignore[func-returns-value]
|
||||||
/home/devmatrix/projects/fan-controller-v2/web_server.py:163: 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:
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/uvicorn/middleware/proxy_headers.py", line 84, in __call__
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
return await self.app(scope, receive, send)
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/fastapi/applications.py", line 1054, in __call__
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
await super().__call__(scope, receive, send)
|
||||||
/home/devmatrix/projects/fan-controller-v2/web_server.py:163: 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).
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/starlette/applications.py", line 123, in __call__
|
||||||
if datetime.utcnow() > expiry:
|
await self.middleware_stack(scope, receive, send)
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/starlette/middleware/errors.py", line 186, in __call__
|
||||||
INFO: 127.0.0.1:41586 - "GET /?theme=light HTTP/1.1" 200 OK
|
raise exc
|
||||||
/home/devmatrix/projects/fan-controller-v2/web_server.py:163: 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).
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/starlette/middleware/errors.py", line 164, in __call__
|
||||||
if datetime.utcnow() > expiry:
|
await self.app(scope, receive, _send)
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/starlette/middleware/cors.py", line 83, in __call__
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
await self.app(scope, receive, send)
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/starlette/middleware/exceptions.py", line 62, in __call__
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app
|
||||||
/home/devmatrix/projects/fan-controller-v2/web_server.py:163: 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).
|
raise exc
|
||||||
if datetime.utcnow() > expiry:
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
await app(scope, receive, sender)
|
||||||
/home/devmatrix/projects/fan-controller-v2/web_server.py:163: 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).
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/starlette/routing.py", line 762, in __call__
|
||||||
if datetime.utcnow() > expiry:
|
await self.middleware_stack(scope, receive, send)
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/starlette/routing.py", line 782, in app
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
await route.handle(scope, receive, send)
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/starlette/routing.py", line 297, in handle
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
await self.app(scope, receive, send)
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/starlette/routing.py", line 77, in app
|
||||||
/home/devmatrix/projects/fan-controller-v2/web_server.py:163: 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).
|
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
|
||||||
if datetime.utcnow() > expiry:
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
raise exc
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
await app(scope, receive, sender)
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/starlette/routing.py", line 72, in app
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
response = await func(request)
|
||||||
/home/devmatrix/projects/fan-controller-v2/web_server.py:163: 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:
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/fastapi/routing.py", line 299, in app
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
raise e
|
||||||
/home/devmatrix/projects/fan-controller-v2/web_server.py:163: 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).
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/fastapi/routing.py", line 294, in app
|
||||||
if datetime.utcnow() > expiry:
|
raw_response = await run_endpoint_function(
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/fastapi/routing.py", line 191, in run_endpoint_function
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
return await dependant.call(**values)
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
/home/devmatrix/projects/fan-controller-v2/web_server.py:163: 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).
|
File "/home/devmatrix/projects/fan-controller-v2/web_server.py", line 1592, in root
|
||||||
if datetime.utcnow() > expiry:
|
return HTMLResponse(content=get_html(theme))
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
^^^^^^^^^^^^^^^
|
||||||
/home/devmatrix/projects/fan-controller-v2/web_server.py:163: 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).
|
File "/home/devmatrix/projects/fan-controller-v2/web_server.py", line 1424, in get_html
|
||||||
if datetime.utcnow() > expiry:
|
<style>body{padding-bottom:80px !important;}</style>
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
^^^^^^^
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
NameError: name 'padding' is not defined
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
INFO: 192.168.5.30:64908 - "GET /http%3A//192.168.5.210%3A3000/favicon.ico HTTP/1.1" 404 Not Found
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
INFO: 192.168.5.30:64908 - "GET /login HTTP/1.1" 200 OK
|
||||||
/home/devmatrix/projects/fan-controller-v2/web_server.py:163: 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).
|
INFO: 192.168.5.30:64908 - "GET /favicon.ico HTTP/1.1" 200 OK
|
||||||
if datetime.utcnow() > expiry:
|
/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).
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
self._sessions[token] = (username, datetime.utcnow() + timedelta(days=7))
|
||||||
/home/devmatrix/projects/fan-controller-v2/web_server.py:163: 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).
|
INFO: 192.168.5.30:52032 - "POST /api/auth/login HTTP/1.1" 200 OK
|
||||||
if datetime.utcnow() > expiry:
|
INFO: 192.168.5.30:52032 - "GET / HTTP/1.1" 500 Internal Server Error
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
ERROR: Exception in ASGI application
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
Traceback (most recent call last):
|
||||||
INFO: 192.168.5.30:55757 - "GET /api/status HTTP/1.1" 200 OK
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/uvicorn/protocols/http/httptools_impl.py", line 419, in run_asgi
|
||||||
|
result = await app( # type: ignore[func-returns-value]
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/uvicorn/middleware/proxy_headers.py", line 84, in __call__
|
||||||
|
return await self.app(scope, receive, send)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/fastapi/applications.py", line 1054, in __call__
|
||||||
|
await super().__call__(scope, receive, send)
|
||||||
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/starlette/applications.py", line 123, in __call__
|
||||||
|
await self.middleware_stack(scope, receive, send)
|
||||||
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/starlette/middleware/errors.py", line 186, in __call__
|
||||||
|
raise exc
|
||||||
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/starlette/middleware/errors.py", line 164, in __call__
|
||||||
|
await self.app(scope, receive, _send)
|
||||||
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/starlette/middleware/cors.py", line 83, in __call__
|
||||||
|
await self.app(scope, receive, send)
|
||||||
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/starlette/middleware/exceptions.py", line 62, in __call__
|
||||||
|
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
|
||||||
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app
|
||||||
|
raise exc
|
||||||
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
|
||||||
|
await app(scope, receive, sender)
|
||||||
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/starlette/routing.py", line 762, in __call__
|
||||||
|
await self.middleware_stack(scope, receive, send)
|
||||||
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/starlette/routing.py", line 782, in app
|
||||||
|
await route.handle(scope, receive, send)
|
||||||
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/starlette/routing.py", line 297, in handle
|
||||||
|
await self.app(scope, receive, send)
|
||||||
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/starlette/routing.py", line 77, in app
|
||||||
|
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
|
||||||
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app
|
||||||
|
raise exc
|
||||||
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
|
||||||
|
await app(scope, receive, sender)
|
||||||
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/starlette/routing.py", line 72, in app
|
||||||
|
response = await func(request)
|
||||||
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/fastapi/routing.py", line 299, in app
|
||||||
|
raise e
|
||||||
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/fastapi/routing.py", line 294, in app
|
||||||
|
raw_response = await run_endpoint_function(
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/devmatrix/.local/lib/python3.12/site-packages/fastapi/routing.py", line 191, in run_endpoint_function
|
||||||
|
return await dependant.call(**values)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/home/devmatrix/projects/fan-controller-v2/web_server.py", line 1592, in root
|
||||||
|
return HTMLResponse(content=get_html(theme))
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
File "/home/devmatrix/projects/fan-controller-v2/web_server.py", line 1424, in get_html
|
||||||
|
<style>body{padding-bottom:80px !important;}</style>
|
||||||
|
^^^^^^^
|
||||||
|
NameError: name 'padding' is not defined
|
||||||
|
INFO: 192.168.5.30:51945 - "GET /http%3A//192.168.5.210%3A3000/favicon.ico HTTP/1.1" 404 Not Found
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue