first commit
This commit is contained in:
558
README.md
Normal file
558
README.md
Normal file
@@ -0,0 +1,558 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|
# 🌡️ Lab Weather Tracker
|
||||||
|
### *Environmental Monitoring System for Raspberry Pi*
|
||||||
|
|
||||||
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
[](https://www.python.org/downloads/)
|
||||||
|
[](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)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 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
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td width="50%">
|
||||||
|
|
||||||
|
### 📊 Data Collection
|
||||||
|
- 🌡️ **Temperature Monitoring** with CPU heat compensation
|
||||||
|
- 💧 **Humidity Tracking** (relative humidity %)
|
||||||
|
- 🔽 **Barometric Pressure** (hPa)
|
||||||
|
- 💡 **Ambient Light** levels (lux)
|
||||||
|
- 🎨 **Color Detection** (RGB values)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td width="50%">
|
||||||
|
|
||||||
|
### 🎯 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
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
### 🔥 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://<raspberry-pi-ip>:5000`
|
||||||
|
|
||||||
|
### Dashboard Features
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td width="50%">
|
||||||
|
|
||||||
|
#### 📊 Current Readings Card
|
||||||
|
- Live temperature (compensated + raw)
|
||||||
|
- Real-time pressure
|
||||||
|
- Current humidity
|
||||||
|
- Light levels
|
||||||
|
- Color swatch visualization
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td width="50%">
|
||||||
|
|
||||||
|
#### 📈 Analytics
|
||||||
|
- Min/Max/Average statistics
|
||||||
|
- Historical trend charts
|
||||||
|
- Last 100 data points
|
||||||
|
- Auto-refresh every 60 seconds
|
||||||
|
- Responsive mobile design
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
### Made with ❤️ by MBTECH
|
||||||
|
|
||||||
|
**MBTECH Binglab Environmental Monitoring System**
|
||||||
|
|
||||||
|
*Keeping your lab conditions perfect, one reading at a time.*
|
||||||
|
|
||||||
|
[](https://github.com/yourusername/raspi-enviro)
|
||||||
|
[](#)
|
||||||
|
[](#)
|
||||||
|
|
||||||
|
</div>
|
||||||
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
pimoroni-bme280>=1.0.0
|
||||||
|
ltr559>=1.0.0
|
||||||
|
smbus2
|
||||||
|
Flask>=2.3.0
|
||||||
218
static/style.css
Normal file
218
static/style.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
366
templates/dashboard.html
Normal file
366
templates/dashboard.html
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Lab Weather Dashboard</title>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="{{ url_for('static', filename='style.css') }}"
|
||||||
|
/>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>MBTECH Binglab Envoro Dashboard</h1>
|
||||||
|
<p class="subtitle">Environmental Monitoring System</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="current-readings">
|
||||||
|
<h2>Current Conditions</h2>
|
||||||
|
<div class="reading-cards">
|
||||||
|
<div class="card temperature-card">
|
||||||
|
<div class="card-icon">🌡️</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="card-value" id="current-temp">--</div>
|
||||||
|
<div class="card-label">Temperature (°C)</div>
|
||||||
|
<div class="card-sublabel" id="temp-raw" style="font-size: 0.85em; color: #888; margin-top: 4px;">Raw: --°C</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card pressure-card">
|
||||||
|
<div class="card-icon">🔽</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="card-value" id="current-pressure">
|
||||||
|
--
|
||||||
|
</div>
|
||||||
|
<div class="card-label">Pressure (hPa)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card light-card">
|
||||||
|
<div class="card-icon">💡</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="card-value" id="current-light">--</div>
|
||||||
|
<div class="card-label">Light (lux)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card color-card">
|
||||||
|
<div class="card-icon">🎨</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="color-swatch" id="color-swatch"></div>
|
||||||
|
<div class="card-label">Detected Color</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="last-update">
|
||||||
|
Last update: <span id="last-update">--</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-section">
|
||||||
|
<h2>Today's Statistics</h2>
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-title">Temperature</div>
|
||||||
|
<div class="stat-values">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">Min:</span>
|
||||||
|
<span class="stat-value" id="temp-min">--</span
|
||||||
|
>°C
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">Max:</span>
|
||||||
|
<span class="stat-value" id="temp-max">--</span
|
||||||
|
>°C
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">Avg:</span>
|
||||||
|
<span class="stat-value" id="temp-avg">--</span
|
||||||
|
>°C
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-title">Pressure</div>
|
||||||
|
<div class="stat-values">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">Min:</span>
|
||||||
|
<span class="stat-value" id="pressure-min"
|
||||||
|
>--</span
|
||||||
|
>
|
||||||
|
hPa
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">Max:</span>
|
||||||
|
<span class="stat-value" id="pressure-max"
|
||||||
|
>--</span
|
||||||
|
>
|
||||||
|
hPa
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">Avg:</span>
|
||||||
|
<span class="stat-value" id="pressure-avg"
|
||||||
|
>--</span
|
||||||
|
>
|
||||||
|
hPa
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-title">Light</div>
|
||||||
|
<div class="stat-values">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">Min:</span>
|
||||||
|
<span class="stat-value" id="light-min"
|
||||||
|
>--</span
|
||||||
|
>
|
||||||
|
lux
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">Max:</span>
|
||||||
|
<span class="stat-value" id="light-max"
|
||||||
|
>--</span
|
||||||
|
>
|
||||||
|
lux
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">Avg:</span>
|
||||||
|
<span class="stat-value" id="light-avg"
|
||||||
|
>--</span
|
||||||
|
>
|
||||||
|
lux
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="charts-section">
|
||||||
|
<h2>Historical Data</h2>
|
||||||
|
<div class="chart-container">
|
||||||
|
<canvas id="temperatureChart"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="chart-container">
|
||||||
|
<canvas id="pressureChart"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="chart-container">
|
||||||
|
<canvas id="lightChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let tempChart, pressureChart, lightChart;
|
||||||
|
|
||||||
|
// Initialize charts
|
||||||
|
function initCharts() {
|
||||||
|
const commonOptions = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: true,
|
||||||
|
aspectRatio: 3,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: "category",
|
||||||
|
ticks: {
|
||||||
|
maxTicksLimit: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
beginAtZero: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
tempChart = new Chart(
|
||||||
|
document.getElementById("temperatureChart"),
|
||||||
|
{
|
||||||
|
type: "line",
|
||||||
|
data: {
|
||||||
|
labels: [],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "Temperature (°C)",
|
||||||
|
data: [],
|
||||||
|
borderColor: "#ff6b6b",
|
||||||
|
backgroundColor: "rgba(255, 107, 107, 0.1)",
|
||||||
|
tension: 0.4,
|
||||||
|
fill: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: commonOptions,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
pressureChart = new Chart(
|
||||||
|
document.getElementById("pressureChart"),
|
||||||
|
{
|
||||||
|
type: "line",
|
||||||
|
data: {
|
||||||
|
labels: [],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "Pressure (hPa)",
|
||||||
|
data: [],
|
||||||
|
borderColor: "#4ecdc4",
|
||||||
|
backgroundColor: "rgba(78, 205, 196, 0.1)",
|
||||||
|
tension: 0.4,
|
||||||
|
fill: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: commonOptions,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
lightChart = new Chart(document.getElementById("lightChart"), {
|
||||||
|
type: "line",
|
||||||
|
data: {
|
||||||
|
labels: [],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "Light (lux)",
|
||||||
|
data: [],
|
||||||
|
borderColor: "#ffe66d",
|
||||||
|
backgroundColor: "rgba(255, 230, 109, 0.1)",
|
||||||
|
tension: 0.4,
|
||||||
|
fill: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: commonOptions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update current readings
|
||||||
|
async function updateCurrent() {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/current");
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
document.getElementById("current-temp").textContent =
|
||||||
|
data.temperature;
|
||||||
|
|
||||||
|
// Update raw temperature if available
|
||||||
|
if (data.temperature_raw !== undefined) {
|
||||||
|
document.getElementById("temp-raw").textContent =
|
||||||
|
`Raw: ${data.temperature_raw}°C`;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("current-pressure").textContent =
|
||||||
|
data.pressure;
|
||||||
|
document.getElementById("current-light").textContent =
|
||||||
|
data.light;
|
||||||
|
document.getElementById("last-update").textContent =
|
||||||
|
data.timestamp;
|
||||||
|
|
||||||
|
// Update color swatch if RGB data is available
|
||||||
|
if (data.rgb) {
|
||||||
|
const colorSwatch = document.getElementById("color-swatch");
|
||||||
|
colorSwatch.style.backgroundColor = `rgb(${data.rgb.r}, ${data.rgb.g}, ${data.rgb.b})`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching current data:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update statistics
|
||||||
|
async function updateStats() {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/stats");
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.temperature) {
|
||||||
|
document.getElementById("temp-min").textContent =
|
||||||
|
data.temperature.min;
|
||||||
|
document.getElementById("temp-max").textContent =
|
||||||
|
data.temperature.max;
|
||||||
|
document.getElementById("temp-avg").textContent =
|
||||||
|
data.temperature.avg;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.pressure) {
|
||||||
|
document.getElementById("pressure-min").textContent =
|
||||||
|
data.pressure.min;
|
||||||
|
document.getElementById("pressure-max").textContent =
|
||||||
|
data.pressure.max;
|
||||||
|
document.getElementById("pressure-avg").textContent =
|
||||||
|
data.pressure.avg;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.light) {
|
||||||
|
document.getElementById("light-min").textContent =
|
||||||
|
data.light.min;
|
||||||
|
document.getElementById("light-max").textContent =
|
||||||
|
data.light.max;
|
||||||
|
document.getElementById("light-avg").textContent =
|
||||||
|
data.light.avg;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching stats:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update historical charts
|
||||||
|
async function updateHistory() {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/history");
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.length === 0) return;
|
||||||
|
|
||||||
|
const labels = data.map((d) => {
|
||||||
|
const date = new Date(d.timestamp);
|
||||||
|
return date.toLocaleTimeString("en-US", {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const temps = data.map((d) => d.temperature);
|
||||||
|
const pressures = data.map((d) => d.pressure);
|
||||||
|
const lights = data.map((d) => d.light);
|
||||||
|
|
||||||
|
tempChart.data.labels = labels;
|
||||||
|
tempChart.data.datasets[0].data = temps;
|
||||||
|
tempChart.update();
|
||||||
|
|
||||||
|
pressureChart.data.labels = labels;
|
||||||
|
pressureChart.data.datasets[0].data = pressures;
|
||||||
|
pressureChart.update();
|
||||||
|
|
||||||
|
lightChart.data.labels = labels;
|
||||||
|
lightChart.data.datasets[0].data = lights;
|
||||||
|
lightChart.update();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching history:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize and start auto-refresh
|
||||||
|
initCharts();
|
||||||
|
updateCurrent();
|
||||||
|
updateStats();
|
||||||
|
updateHistory();
|
||||||
|
|
||||||
|
// Refresh current readings every 10 seconds
|
||||||
|
setInterval(updateCurrent, 10000);
|
||||||
|
|
||||||
|
// Refresh stats and history every 60 seconds
|
||||||
|
setInterval(() => {
|
||||||
|
updateStats();
|
||||||
|
updateHistory();
|
||||||
|
}, 60000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
196
weather_tracker.py
Normal file
196
weather_tracker.py
Normal file
@@ -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()
|
||||||
212
web_dashboard.py
Normal file
212
web_dashboard.py
Normal file
@@ -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://<raspberry-pi-ip>: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)
|
||||||
Reference in New Issue
Block a user