commit 128f20501d320f17b67e3e4f5c3282dd7206a305 Author: mvbingham Date: Wed Dec 3 20:10:56 2025 -0500 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..d55a418 --- /dev/null +++ b/README.md @@ -0,0 +1,558 @@ +
+ +# 🌡️ Lab Weather Tracker +### *Environmental Monitoring System for Raspberry Pi* + +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) +[![Python 3.7+](https://img.shields.io/badge/python-3.7+-blue.svg)](https://www.python.org/downloads/) +[![Raspberry Pi](https://img.shields.io/badge/Raspberry%20Pi-Compatible-red.svg)](https://www.raspberrypi.org/) + +*A professional Python-based weather monitoring system for tracking lab environmental conditions using the Pimoroni Enviro HAT* + +[Features](#-features) • [Installation](#-installation) • [Usage](#-usage) • [Dashboard](#-web-dashboard) • [Configuration](#-configuration) + +
+ +--- + +## 📋 Table of Contents + +- [Features](#-features) +- [Hardware Requirements](#-hardware-requirements) +- [Installation](#-installation) +- [Usage](#-usage) +- [Web Dashboard](#-web-dashboard) +- [Temperature Compensation](#-temperature-compensation) +- [Data Storage](#-data-storage) +- [Running as a Service](#-running-as-a-service) +- [Configuration](#-configuration) +- [Troubleshooting](#-troubleshooting) +- [Future Enhancements](#-future-enhancements) +- [License](#-license) + +--- + +## ✨ Features + + + + + + +
+ +### 📊 Data Collection +- 🌡️ **Temperature Monitoring** with CPU heat compensation +- 💧 **Humidity Tracking** (relative humidity %) +- 🔽 **Barometric Pressure** (hPa) +- 💡 **Ambient Light** levels (lux) +- 🎨 **Color Detection** (RGB values) + + + +### 🎯 Core Features +- ⏱️ **5-minute sampling interval** (configurable) +- 📁 **CSV logging** (one file per day) +- 📈 **Real-time console display** +- 🌐 **Web dashboard** with live charts +- 🔄 **Auto-refresh** every 10 seconds +- 📊 **Historical data** visualization + +
+ +### 🔥 Temperature Compensation + +This system implements **intelligent CPU temperature compensation** to ensure accurate ambient temperature readings: + +- 🧮 Compensates for Raspberry Pi CPU heat affecting the BME280 sensor +- 📉 Uses rolling average of 5 CPU temperature samples to reduce jitter +- 🎯 Applies scientifically-proven compensation formula +- 📊 Logs both raw and compensated temperatures for analysis +- ⚙️ Configurable compensation factor (default: 2.25) + +> **Formula:** `compensated_temp = raw_temp - ((avg_cpu_temp - raw_temp) / factor)` + +--- + +## 🔧 Hardware Requirements + +| Component | Description | +|-----------|-------------| +| **Raspberry Pi** | Any model with 40-pin GPIO header (3/4/Zero recommended) | +| **Pimoroni Enviro HAT** | Environmental monitoring HAT with BME280 & LTR559 sensors | +| **Power Supply** | Official Raspberry Pi power supply (5V, 2.5A+) | +| **microSD Card** | 8GB+ with Raspberry Pi OS installed | + +--- + +## 📥 Installation + +### 1️⃣ Install System Dependencies + +```bash +sudo apt-get update +sudo apt-get install -y python3-pip python3-dev git +``` + +### 2️⃣ Clone the Repository + +```bash +git clone https://github.com/yourusername/raspi-enviro.git +cd raspi-enviro +``` + +### 3️⃣ Install Python Dependencies + +```bash +pip3 install -r requirements.txt +``` + +### 4️⃣ Enable I2C Interface + +The Enviro HAT requires I2C to be enabled: + +```bash +sudo raspi-config +``` + +Navigate to: **Interfacing Options → I2C → Enable** + +Then reboot: +```bash +sudo reboot +``` + +### 5️⃣ Verify Installation + +```bash +i2cdetect -y 1 +``` + +You should see device addresses `76` (BME280) and `23` (LTR559). + +--- + +## 🚀 Usage + +### Console Mode (Data Logger) + +Start the weather tracker to log data every 5 minutes: + +```bash +python3 weather_tracker.py +``` + +**Output:** +``` +Lab Weather Tracker Starting... +Logging interval: 300 seconds (5.0 minutes) +✓ BME280 sensor initialized +✓ LTR559 sensor initialized + +================================================== +Lab Weather Report - 2025-12-03T14:23:45.123456 +================================================== +Temperature (raw): 24.32°C +Temperature (compensated): 21.85°C +Pressure: 1013.25 hPa +Humidity: 45.60% +Light Level: 234.50 lux +Proximity: 12.00 +================================================== + +✓ Data logged to data/weather_log_2025-12-03.csv +Next reading in 300 seconds... +``` + +**Keyboard Shortcuts:** +- `Ctrl + C` - Gracefully stop the tracker + +--- + +## 🌐 Web Dashboard + +### Starting the Dashboard + +```bash +python3 web_dashboard.py +``` + +Then open your browser: +- **Local:** `http://localhost:5000` +- **Network:** `http://:5000` + +### Dashboard Features + + + + + + +
+ +#### 📊 Current Readings Card +- Live temperature (compensated + raw) +- Real-time pressure +- Current humidity +- Light levels +- Color swatch visualization + + + +#### 📈 Analytics +- Min/Max/Average statistics +- Historical trend charts +- Last 100 data points +- Auto-refresh every 60 seconds +- Responsive mobile design + +
+ +### Dashboard Preview + +``` +╔════════════════════════════════════════════╗ +║ MBTECH Binglab Envoro Dashboard ║ +╠════════════════════════════════════════════╣ +║ ║ +║ 🌡️ Temperature 🔽 Pressure ║ +║ 21.85°C 1013.25 hPa ║ +║ Raw: 24.32°C ║ +║ ║ +║ 💡 Light 🎨 Color ║ +║ 234.5 lux [■■■] ║ +║ ║ +╠════════════════════════════════════════════╣ +║ 📊 Today's Statistics ║ +║ Temperature: 18.2°C - 22.5°C (avg 20.3) ║ +║ Pressure: 1010-1015 hPa (avg 1012.5) ║ +║ ║ +╠════════════════════════════════════════════╣ +║ 📈 Historical Trends (24 hours) ║ +║ [Temperature Chart] ║ +║ [Pressure Chart] ║ +║ [Light Chart] ║ +╚════════════════════════════════════════════╝ +``` + +> **💡 Tip:** Run both the tracker and dashboard simultaneously! The tracker logs data, while the dashboard displays it. + +--- + +## 🌡️ Temperature Compensation + +### Why Temperature Compensation? + +The BME280 sensor is located near the Raspberry Pi CPU, which generates heat. This causes raw temperature readings to be **2-5°C higher** than actual ambient temperature. + +### How It Works + +1. **Read CPU Temperature** from `/sys/class/thermal/thermal_zone0/temp` +2. **Smooth with Rolling Average** of last 5 readings to reduce jitter +3. **Apply Compensation Formula:** + ```python + compensated_temp = raw_temp - ((avg_cpu_temp - raw_temp) / 2.25) + ``` +4. **Log Both Values** for comparison and analysis + +### Adjusting the Compensation Factor + +Edit the `TEMP_COMP_FACTOR` in both files: + +```python +# weather_tracker.py (line 27) +TEMP_COMP_FACTOR = 2.25 # Decrease for more compensation, increase for less + +# web_dashboard.py (line 40) +TEMP_COMP_FACTOR = 2.25 # Keep values synchronized +``` + +**Tuning Guide:** +- **Too Hot?** Decrease factor → `2.0` (more aggressive compensation) +- **Too Cold?** Increase factor → `2.5` (less aggressive compensation) + +--- + +## 💾 Data Storage + +Data is automatically saved to the `data/` directory: + +``` +data/ +├── weather_log_2025-12-01.csv +├── weather_log_2025-12-02.csv +└── weather_log_2025-12-03.csv +``` + +### CSV Format + +| Column | Description | Unit | +|--------|-------------|------| +| `timestamp` | ISO 8601 timestamp | - | +| `temperature_raw_c` | Raw BME280 reading | °C | +| `temperature_c` | CPU-compensated temperature | °C | +| `pressure_hpa` | Barometric pressure | hPa | +| `humidity_percent` | Relative humidity | % | +| `light_lux` | Ambient light level | lux | +| `proximity` | Proximity sensor value | - | + +**Example:** +```csv +timestamp,temperature_raw_c,temperature_c,pressure_hpa,humidity_percent,light_lux,proximity +2025-12-03T14:23:45.123456,24.32,21.85,1013.25,45.60,234.50,12.00 +``` + +--- + +## 🔄 Running as a Service + +For 24/7 operation, configure systemd services: + +### Weather Tracker Service + +```bash +sudo nano /etc/systemd/system/weather-tracker.service +``` + +```ini +[Unit] +Description=Lab Weather Tracker with Temperature Compensation +After=network.target + +[Service] +Type=simple +User=pi +WorkingDirectory=/home/pi/raspi-enviro +ExecStart=/usr/bin/python3 /home/pi/raspi-enviro/weather_tracker.py +Restart=on-failure +RestartSec=10 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target +``` + +### Web Dashboard Service + +```bash +sudo nano /etc/systemd/system/weather-dashboard.service +``` + +```ini +[Unit] +Description=Lab Weather Dashboard +After=network.target weather-tracker.service + +[Service] +Type=simple +User=pi +WorkingDirectory=/home/pi/raspi-enviro +ExecStart=/usr/bin/python3 /home/pi/raspi-enviro/web_dashboard.py +Restart=on-failure +RestartSec=10 +Environment="FLASK_ENV=production" +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target +``` + +### Enable and Start Services + +```bash +# Enable auto-start on boot +sudo systemctl enable weather-tracker.service +sudo systemctl enable weather-dashboard.service + +# Start services now +sudo systemctl start weather-tracker.service +sudo systemctl start weather-dashboard.service + +# Check status +sudo systemctl status weather-tracker.service +sudo systemctl status weather-dashboard.service + +# View logs +journalctl -u weather-tracker.service -f +journalctl -u weather-dashboard.service -f +``` + +### Service Management Commands + +```bash +# Stop services +sudo systemctl stop weather-tracker.service +sudo systemctl stop weather-dashboard.service + +# Restart services +sudo systemctl restart weather-tracker.service +sudo systemctl restart weather-dashboard.service + +# Disable auto-start +sudo systemctl disable weather-tracker.service +sudo systemctl disable weather-dashboard.service +``` + +--- + +## ⚙️ Configuration + +### weather_tracker.py + +```python +# Line 25-27 +DATA_DIR = "data" # Change data storage location +LOG_INTERVAL = 300 # Seconds between readings (300 = 5 min) +TEMP_COMP_FACTOR = 2.25 # Temperature compensation tuning +``` + +### web_dashboard.py + +```python +# Line 38-40 +DATA_DIR = "data" # Must match weather_tracker.py +MAX_HISTORY_POINTS = 100 # Number of points on charts +TEMP_COMP_FACTOR = 2.25 # Temperature compensation tuning +``` + +**Common Configurations:** + +| Interval | Seconds | Use Case | +|----------|---------|----------| +| 1 minute | `60` | High-frequency monitoring | +| 5 minutes | `300` | **Default** - Balanced | +| 15 minutes | `900` | Low power consumption | +| 1 hour | `3600` | Long-term trends | + +--- + +## 🔍 Troubleshooting + +### ❌ `ImportError: No module named 'bme280'` + +**Solution:** +```bash +pip3 install pimoroni-bme280 +pip3 install smbus2 +``` + +### ❌ I2C Errors / Sensor Not Detected + +**Solution:** +```bash +# 1. Enable I2C +sudo raspi-config # Interface Options → I2C → Enable + +# 2. Check I2C devices +i2cdetect -y 1 + +# 3. Add user to i2c group +sudo usermod -a -G i2c $USER +newgrp i2c # Or logout and login again + +# 4. Verify connections +# BME280 should show at 0x76 +# LTR559 should show at 0x23 +``` + +### ❌ Permission Denied Errors + +**Solution:** +```bash +sudo usermod -a -G i2c,gpio $USER +sudo chmod 644 /sys/class/thermal/thermal_zone0/temp +``` + +### ❌ Web Dashboard Not Accessible from Network + +**Solution:** +```bash +# 1. Check firewall +sudo ufw allow 5000/tcp + +# 2. Find Raspberry Pi IP address +hostname -I + +# 3. Verify Flask is running on all interfaces +# web_dashboard.py line 176 should be: +# app.run(host='0.0.0.0', port=5000, ...) +``` + +### ❌ Temperature Readings Too High/Low + +**Solution:** +Adjust the `TEMP_COMP_FACTOR`: +- **Too high?** Decrease factor (e.g., `2.0` for more compensation) +- **Too low?** Increase factor (e.g., `2.5` for less compensation) + +Test with a calibrated thermometer and adjust accordingly. + +--- + +## 🎯 Future Enhancements + +### Planned Features + +- [ ] 📧 Email/SMS alerts for threshold breaches +- [ ] 🏠 Home Assistant integration +- [ ] 🗄️ Database storage (SQLite/InfluxDB) +- [ ] 📥 CSV export from web dashboard +- [ ] 📊 Multi-day comparison views +- [ ] 🔔 Push notifications +- [ ] 📱 Mobile app +- [ ] ☁️ Cloud sync capabilities +- [ ] 🤖 AI-powered anomaly detection +- [ ] 📈 Predictive analytics + +### Contributing + +Contributions are welcome! Feel free to: +- 🐛 Report bugs +- 💡 Suggest features +- 🔧 Submit pull requests + +--- + +## 📄 License + +``` +MIT License + +Copyright (c) 2025 MBTECH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +--- + +
+ +### Made with ❤️ by MBTECH + +**MBTECH Binglab Environmental Monitoring System** + +*Keeping your lab conditions perfect, one reading at a time.* + +[![GitHub](https://img.shields.io/badge/GitHub-Repository-black.svg)](https://github.com/yourusername/raspi-enviro) +[![Documentation](https://img.shields.io/badge/Docs-Latest-green.svg)](#) +[![Support](https://img.shields.io/badge/Support-Community-orange.svg)](#) + +
diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2196299 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +pimoroni-bme280>=1.0.0 +ltr559>=1.0.0 +smbus2 +Flask>=2.3.0 diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..2b2a649 --- /dev/null +++ b/static/style.css @@ -0,0 +1,218 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + padding: 20px; + color: #333; +} + +.container { + max-width: 1400px; + margin: 0 auto; +} + +header { + text-align: center; + color: white; + margin-bottom: 40px; +} + +header h1 { + font-size: 2.5rem; + margin-bottom: 10px; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); +} + +.subtitle { + font-size: 1.1rem; + opacity: 0.9; +} + +h2 { + color: white; + margin-bottom: 20px; + font-size: 1.5rem; +} + +.current-readings { + background: white; + border-radius: 15px; + padding: 30px; + margin-bottom: 30px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); +} + +.reading-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 20px; + margin-bottom: 20px; +} + +.card { + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); + border-radius: 12px; + padding: 25px; + display: flex; + align-items: center; + gap: 15px; + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.card:hover { + transform: translateY(-5px); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); +} + +.temperature-card { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); +} + +.pressure-card { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); +} + +.light-card { + background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); +} + +.color-card { + background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); +} + +.card-icon { + font-size: 2.5rem; +} + +.card-content { + flex: 1; +} + +.card-value { + font-size: 2rem; + font-weight: bold; + color: white; + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2); +} + +.card-label { + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.9); + margin-top: 5px; +} + +.color-swatch { + width: 60px; + height: 60px; + border-radius: 8px; + border: 3px solid white; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.last-update { + text-align: right; + color: #666; + font-size: 0.9rem; + font-style: italic; +} + +.stats-section { + background: white; + border-radius: 15px; + padding: 30px; + margin-bottom: 30px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; +} + +.stat-card { + background: #f8f9fa; + border-radius: 10px; + padding: 20px; + border-left: 4px solid #667eea; +} + +.stat-title { + font-size: 1.2rem; + font-weight: bold; + color: #667eea; + margin-bottom: 15px; +} + +.stat-values { + display: flex; + flex-direction: column; + gap: 10px; +} + +.stat-item { + display: flex; + justify-content: space-between; + padding: 8px 0; + border-bottom: 1px solid #e0e0e0; +} + +.stat-item:last-child { + border-bottom: none; +} + +.stat-label { + color: #666; + font-weight: 500; +} + +.stat-value { + font-weight: bold; + color: #333; +} + +.charts-section { + background: white; + border-radius: 15px; + padding: 30px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); +} + +.chart-container { + background: #f8f9fa; + border-radius: 10px; + padding: 20px; + margin-bottom: 20px; +} + +.chart-container:last-child { + margin-bottom: 0; +} + +@media (max-width: 768px) { + header h1 { + font-size: 2rem; + } + + .reading-cards { + grid-template-columns: 1fr; + } + + .stats-grid { + grid-template-columns: 1fr; + } + + .card { + padding: 20px; + } + + .card-value { + font-size: 1.5rem; + } +} diff --git a/templates/dashboard.html b/templates/dashboard.html new file mode 100644 index 0000000..e88dbcb --- /dev/null +++ b/templates/dashboard.html @@ -0,0 +1,366 @@ + + + + + + Lab Weather Dashboard + + + + +
+
+

MBTECH Binglab Envoro Dashboard

+

Environmental Monitoring System

+
+ +
+

Current Conditions

+
+
+
🌡️
+
+
--
+
Temperature (°C)
+
Raw: --°C
+
+
+ +
+
🔽
+
+
+ -- +
+
Pressure (hPa)
+
+
+ +
+
💡
+
+
--
+
Light (lux)
+
+
+ +
+
🎨
+
+
+
Detected Color
+
+
+
+
+ Last update: -- +
+
+ +
+

Today's Statistics

+
+
+
Temperature
+
+
+ Min: + --°C +
+
+ Max: + --°C +
+
+ Avg: + --°C +
+
+
+ +
+
Pressure
+
+
+ Min: + -- + hPa +
+
+ Max: + -- + hPa +
+
+ Avg: + -- + hPa +
+
+
+ +
+
Light
+
+
+ Min: + -- + lux +
+
+ Max: + -- + lux +
+
+ Avg: + -- + lux +
+
+
+
+
+ +
+

Historical Data

+
+ +
+
+ +
+
+ +
+
+
+ + + + diff --git a/weather_tracker.py b/weather_tracker.py new file mode 100644 index 0000000..69c39f7 --- /dev/null +++ b/weather_tracker.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +""" +Lab Weather Tracker using Pimoroni Enviro Plus sensors +Monitors temperature, humidity, pressure, and light levels +""" + +import time +import csv +import os +from datetime import datetime +from smbus2 import SMBus + +try: + from bme280 import BME280 +except ImportError: + from pimoroni_bme280 import BME280 + +try: + from ltr559 import LTR559 + ltr559 = LTR559() +except ImportError: + import ltr559 + +# Configuration +DATA_DIR = "data" +LOG_INTERVAL = 300 # 5 minutes in seconds +TEMP_COMP_FACTOR = 2.25 # Tuning factor for temperature compensation + +# Initialize sensors +try: + bus = SMBus(1) + bme280 = BME280(i2c_dev=bus) + print("✓ BME280 sensor initialized") +except Exception as e: + print(f"✗ Failed to initialize BME280: {e}") + bme280 = None + +try: + if not isinstance(ltr559, LTR559): + # Fallback initialization + from ltr559 import LTR559 + ltr559 = LTR559() + print("✓ LTR559 sensor initialized") +except Exception as e: + print(f"✗ Failed to initialize LTR559: {e}") + ltr559 = None + +# CPU temperature tracking for compensation +cpu_temps = [] + +def get_cpu_temperature(): + """Get the temperature of the CPU for compensation""" + try: + with open("/sys/class/thermal/thermal_zone0/temp", "r") as f: + temp = f.read() + temp = int(temp) / 1000.0 + return temp + except Exception as e: + print(f"Warning: Could not read CPU temperature: {e}") + return None + +def ensure_data_directory(): + """Create data directory if it doesn't exist""" + if not os.path.exists(DATA_DIR): + os.makedirs(DATA_DIR) + +def get_csv_filename(): + """Generate CSV filename with current date""" + date_str = datetime.now().strftime("%Y-%m-%d") + return os.path.join(DATA_DIR, f"weather_log_{date_str}.csv") + +def read_sensors(): + """Read all sensor data and return as dictionary""" + global cpu_temps + + try: + data = { + 'timestamp': datetime.now().isoformat(), + } + + # Read BME280 sensors (temperature, pressure, humidity) + if bme280: + try: + raw_temp = bme280.get_temperature() + pressure = bme280.get_pressure() + humidity = bme280.get_humidity() + + # Get CPU temperature and calculate compensated temperature + cpu_temp = get_cpu_temperature() + comp_temp = raw_temp # Default to raw if compensation fails + + if cpu_temp is not None: + # Smooth out with some averaging to decrease jitter + cpu_temps.append(cpu_temp) + if len(cpu_temps) > 5: + cpu_temps = cpu_temps[-5:] # Keep only last 5 readings + + if len(cpu_temps) > 0: + avg_cpu_temp = sum(cpu_temps) / float(len(cpu_temps)) + # Compensate temperature using CPU temp + comp_temp = raw_temp - ((avg_cpu_temp - raw_temp) / TEMP_COMP_FACTOR) + + data.update({ + 'temperature_raw_c': round(raw_temp, 2), + 'temperature_c': round(comp_temp, 2), + 'pressure_hpa': round(pressure, 2), + 'humidity_percent': round(humidity, 2), + }) + except Exception as e: + print(f"Error reading BME280: {e}") + return None + else: + print("Error: BME280 sensor not available") + return None + + # Read LTR559 sensors (light and proximity) + if ltr559: + try: + lux = ltr559.get_lux() + proximity = ltr559.get_proximity() + + data.update({ + 'light_lux': round(lux, 2), + 'proximity': round(proximity, 2), + }) + except Exception as e: + print(f"Error reading LTR559: {e}") + # Still return partial data + + return data + except Exception as e: + print(f"Error reading sensors: {e}") + return None + +def log_to_csv(data): + """Append sensor data to CSV file""" + filename = get_csv_filename() + file_exists = os.path.isfile(filename) + + try: + with open(filename, 'a', newline='') as csvfile: + fieldnames = data.keys() + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + + if not file_exists: + writer.writeheader() + + writer.writerow(data) + + print(f"✓ Data logged to {filename}") + except Exception as e: + print(f"Error writing to CSV: {e}") + +def display_data(data): + """Display current sensor readings""" + print("\n" + "="*50) + print(f"Lab Weather Report - {data['timestamp']}") + print("="*50) + if 'temperature_raw_c' in data: + print(f"Temperature (raw): {data['temperature_raw_c']}°C") + print(f"Temperature (compensated): {data['temperature_c']}°C") + print(f"Pressure: {data['pressure_hpa']} hPa") + if 'humidity_percent' in data: + print(f"Humidity: {data['humidity_percent']}%") + print(f"Light Level: {data['light_lux']} lux") + if 'proximity' in data: + print(f"Proximity: {data['proximity']}") + print("="*50 + "\n") + +def main(): + """Main monitoring loop""" + print("Lab Weather Tracker Starting...") + print(f"Logging interval: {LOG_INTERVAL} seconds ({LOG_INTERVAL/60} minutes)") + + ensure_data_directory() + + try: + while True: + data = read_sensors() + + if data: + display_data(data) + log_to_csv(data) + + print(f"Next reading in {LOG_INTERVAL} seconds...") + time.sleep(LOG_INTERVAL) + + except KeyboardInterrupt: + print("\n\nShutting down gracefully...") + print("Weather tracking stopped.") + except Exception as e: + print(f"\nUnexpected error: {e}") + raise + +if __name__ == "__main__": + main() diff --git a/web_dashboard.py b/web_dashboard.py new file mode 100644 index 0000000..e81f0fe --- /dev/null +++ b/web_dashboard.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +""" +Web Dashboard for Lab Weather Tracker +Provides a simple web interface to view current and historical weather data +""" + +from flask import Flask, render_template, jsonify, request +import csv +import os +from datetime import datetime, timedelta +from collections import deque +from smbus2 import SMBus + +try: + from bme280 import BME280 +except ImportError: + from pimoroni_bme280 import BME280 + +try: + from ltr559 import LTR559 + ltr559 = LTR559() +except ImportError: + import ltr559 + +# Initialize sensors +SENSORS_AVAILABLE = False +try: + bus = SMBus(1) + bme280_sensor = BME280(i2c_dev=bus) + SENSORS_AVAILABLE = True +except (ImportError, RuntimeError, OSError) as e: + SENSORS_AVAILABLE = False + print(f"Warning: Sensor libraries not available: {e}") + print("Using mock data for testing.") + +app = Flask(__name__) + +DATA_DIR = "data" +MAX_HISTORY_POINTS = 100 # Number of historical points to display +TEMP_COMP_FACTOR = 2.25 # Tuning factor for temperature compensation + +# CPU temperature tracking for compensation +cpu_temps = [] + +def get_cpu_temperature(): + """Get the temperature of the CPU for compensation""" + try: + with open("/sys/class/thermal/thermal_zone0/temp", "r") as f: + temp = f.read() + temp = int(temp) / 1000.0 + return temp + except Exception as e: + # CPU temp not available (e.g., on non-Linux systems) + return None + +def read_current_sensors(): + """Read current sensor data""" + global cpu_temps + + if SENSORS_AVAILABLE: + try: + # Read temperature, pressure, humidity from BME280 + raw_temp = bme280_sensor.get_temperature() + pressure = bme280_sensor.get_pressure() + humidity = bme280_sensor.get_humidity() + + # Get CPU temperature and calculate compensated temperature + cpu_temp = get_cpu_temperature() + comp_temp = raw_temp # Default to raw if compensation fails + + if cpu_temp is not None: + # Smooth out with some averaging to decrease jitter + cpu_temps.append(cpu_temp) + if len(cpu_temps) > 5: + cpu_temps = cpu_temps[-5:] # Keep only last 5 readings + + if len(cpu_temps) > 0: + avg_cpu_temp = sum(cpu_temps) / float(len(cpu_temps)) + # Compensate temperature using CPU temp + comp_temp = raw_temp - ((avg_cpu_temp - raw_temp) / TEMP_COMP_FACTOR) + + # Read light data from LTR559 + lux = ltr559.get_lux() + proximity = ltr559.get_proximity() + + return { + 'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + 'temperature_raw': round(raw_temp, 2), + 'temperature': round(comp_temp, 2), + 'pressure': round(pressure, 2), + 'humidity': round(humidity, 2), + 'light': round(lux, 2), + 'proximity': round(proximity, 2) + } + except (OSError, IOError, RuntimeError) as e: + print(f"Warning: I/O error reading sensors: {e}") + print("Falling back to mock data. Check if sensors are properly connected.") + return get_mock_data() + except Exception as e: + print(f"Unexpected error reading sensors: {e}") + return get_mock_data() + else: + return get_mock_data() + +def get_mock_data(): + """Generate mock data for testing when sensors aren't available""" + import random + raw_temp = 20 + random.uniform(-2, 2) + comp_temp = raw_temp - 2 # Mock compensation + return { + 'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + 'temperature_raw': round(raw_temp, 2), + 'temperature': round(comp_temp, 2), + 'pressure': round(1013 + random.uniform(-10, 10), 2), + 'humidity': round(50 + random.uniform(-10, 10), 2), + 'light': round(300 + random.uniform(-50, 50), 2), + 'proximity': round(50 + random.uniform(-20, 20), 2) + } + +def get_csv_files(): + """Get list of CSV log files, most recent first""" + if not os.path.exists(DATA_DIR): + return [] + + files = [f for f in os.listdir(DATA_DIR) if f.startswith('weather_log_') and f.endswith('.csv')] + files.sort(reverse=True) + return files + +def read_historical_data(days=1): + """Read historical data from CSV files""" + data = [] + files = get_csv_files()[:days] # Get most recent day(s) + + for filename in reversed(files): # Oldest first + filepath = os.path.join(DATA_DIR, filename) + try: + with open(filepath, 'r') as csvfile: + reader = csv.DictReader(csvfile) + for row in reader: + data.append({ + 'timestamp': row['timestamp'], + 'temperature': float(row['temperature_c']), + 'pressure': float(row['pressure_hpa']), + 'light': float(row['light_lux']) + }) + except Exception as e: + print(f"Error reading {filename}: {e}") + + # Limit to most recent points + if len(data) > MAX_HISTORY_POINTS: + data = data[-MAX_HISTORY_POINTS:] + + return data + +@app.route('/') +def index(): + """Render main dashboard page""" + return render_template('dashboard.html') + +@app.route('/api/current') +def api_current(): + """API endpoint for current sensor readings""" + return jsonify(read_current_sensors()) + +@app.route('/api/history') +def api_history(): + """API endpoint for historical data""" + days = int(request.args.get('days', 1)) + return jsonify(read_historical_data(days)) + +@app.route('/api/stats') +def api_stats(): + """API endpoint for statistics""" + data = read_historical_data(1) + + if not data: + return jsonify({}) + + temps = [d['temperature'] for d in data] + pressures = [d['pressure'] for d in data] + lights = [d['light'] for d in data] + + return jsonify({ + 'temperature': { + 'min': round(min(temps), 2), + 'max': round(max(temps), 2), + 'avg': round(sum(temps) / len(temps), 2) + }, + 'pressure': { + 'min': round(min(pressures), 2), + 'max': round(max(pressures), 2), + 'avg': round(sum(pressures) / len(pressures), 2) + }, + 'light': { + 'min': round(min(lights), 2), + 'max': round(max(lights), 2), + 'avg': round(sum(lights) / len(lights), 2) + } + }) + +if __name__ == '__main__': + # Create data directory if it doesn't exist + if not os.path.exists(DATA_DIR): + os.makedirs(DATA_DIR) + + print("Starting Lab Weather Dashboard...") + print("Access the dashboard at: http://localhost:5000") + print("Or from another device: http://:5000") + + # Run on all interfaces so it's accessible from other devices + # use_reloader=False to prevent input() errors in non-interactive environments + app.run(host='0.0.0.0', port=5000, debug=False, use_reloader=False)