1. 项目背景与核心价值
去年装修新房时,我发现自己对室内环境数据异常敏感。每当PM2.5超过50或温度低于20℃时,过敏性鼻炎就会发作。市面上的环境监测设备要么功能单一,要么数据封闭,于是我用Python搭建了这套环境感知系统。它不仅能实时采集温湿度、空气质量等8项指标,还通过可视化界面呈现数据趋势,当检测到异常时自动推送预警到手机。
这个项目的独特之处在于:
- 采用模块化设计,传感器类型可自由扩展
- 数据存储使用轻量级SQLite,支持历史查询
- 可视化面板适配移动端,外出也能随时查看
- 预警机制包含阈值触发和突变检测两种模式
2. 硬件选型与数据采集
2.1 传感器组合方案
经过对比测试,最终确定的硬件配置如下表所示:
| 传感器类型 |
型号 |
测量范围 |
精度 |
通信协议 |
| 温湿度 |
SHT31 |
-40~125℃, 0-100%RH |
±0.2℃, ±2%RH |
I2C |
| PM2.5/PM10 |
PMS5003 |
0-1000μg/m³ |
±10% |
UART |
| CO2 |
MH-Z19B |
0-5000ppm |
±50ppm+5% |
UART |
| 甲醛 |
ZE08-CH2O |
0-5ppm |
±0.05ppm |
UART |
注意:选购传感器时建议优先考虑工业级型号,虽然价格是消费级的2-3倍,但长期运行稳定性更好。我曾因贪便宜买过某国产替代型号,结果甲醛传感器三个月后就出现5%的零点漂移。
2.2 树莓派驱动开发
使用Python的smbus2库操作I2C设备时,需要特别注意寄存器读写时序。以下是读取SHT31温度的典型代码:
PYTHON
5
def __init__(self, bus=1, address=0x44):
6
self.bus = smbus2.SMBus(bus)
9
def read_temperature(self):
11
self.bus.write_i2c_block_data(self.addr, 0x2C, [0x06])
14
data = self.bus.read_i2c_block_data(self.addr, 0x00, 6)
16
raw_temp = (data[0] << 8) | data[1]
17
return -45 + 175 * (raw_temp / 65535.0)
21
print(f"当前温度: {sensor.read_temperature():.1f}℃")
UART设备驱动建议使用pyserial库。针对PMS5003颗粒物传感器,需要处理其特有的数据帧结构:
PYTHON
2
from struct import unpack
4
def read_pm25(port='/dev/ttyAMA0'):
5
ser = serial.Serial(port, baudrate=9600, timeout=1)
8
if ser.read() == b'\x42':
9
if ser.read() == b'\x4d':
13
pm25 = (data[10] << 8) | data[11]
3. 数据存储与处理架构
3.1 数据库设计
采用SQLite作为本地存储方案,表结构设计如下:
SQL
1
CREATE TABLE environment_data (
2
id INTEGER PRIMARY KEY AUTOINCREMENT,
3
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
14
CREATE TABLE alert_logs (
15
id INTEGER PRIMARY KEY,
16
alert_type VARCHAR(20),
使用Python的sqlite3模块时,有两个性能优化技巧:
- 启用WAL模式:
conn.execute('PRAGMA journal_mode=WAL')
- 批量插入时使用事务:
PYTHON
1
def batch_insert(data_list):
2
conn = sqlite3.connect('sensor.db')
4
conn.executemany("""INSERT INTO environment_data
5
(temperature, humidity, pm25)
6
VALUES (?, ?, ?)""", data_list)
3.2 数据预处理流程
原始传感器数据需经过以下处理环节:
- 异常值过滤:采用滑动窗口法,剔除超过3倍标准差的数据点
- 单位统一转换:如甲醛传感器原始输出为ppb,需转换为ppm
- 时间对齐:多传感器采样周期不同,按1分钟间隔重新采样
- 空气质量指数(AQI)计算:
PYTHON
1
def calculate_aqi(pm25, pm10):
4
aqi_pm25 = (50/35)*pm25
6
aqi_pm25 = 50 + (50/40)*(pm25-35)
14
return max(aqi_pm25, aqi_pm10)
4. 可视化系统实现
4.1 Web界面搭建
使用Flask+ECharts构建可视化面板,核心路由设计:
PYTHON
1
from flask import Flask, render_template
6
@app.route('/dashboard')
8
conn = sqlite3.connect('sensor.db')
12
cursor.execute("""SELECT strftime('%H:%M',timestamp),
13
temperature, humidity, pm25
15
WHERE timestamp > datetime('now','-24 hours')""")
16
data = cursor.fetchall()
19
hours = [row[0] for row in data]
20
temp = [row[1] for row in data]
21
return render_template('dashboard.html',
22
hours=hours, temp=temp)
24
if __name__ == '__main__':
25
app.run(host='0.0.0.0', port=5000)
前端采用ECharts实现动态图表,关键配置项:
JAVASCRIPT
2
tooltip: { trigger: 'axis' },
3
xAxis: { type: 'category', data: hours },
4
yAxis: { type: 'value' },
9
lineStyle: { width: 3 },
12
{ type: 'max', name: '最大值' },
13
{ type: 'min', name: '最小值' }
4.2 移动端适配技巧
通过CSS媒体查询实现响应式布局:
CSS
7
@media (max-width: 768px) {
14
flex-direction: column;
使用Flexbox布局确保元素自适应:
HTML
1
<div class="dashboard">
2
<div class="chart-container" id="temp-chart"></div>
3
<div class="control-panel">
4
<button class="time-range" data-hours="24">24小时</button>
5
<button class="time-range" data-hours="72">3天</button>
5. 异常检测与预警机制
5.1 阈值触发策略
在config.py中定义各指标的警戒阈值:
PYTHON
2
'temperature': {'min': 18, 'max': 28},
3
'humidity': {'min': 30, 'max': 70},
预警判断逻辑实现:
PYTHON
1
def check_thresholds(current_values):
3
for metric, value in current_values.items():
4
if metric in ALERT_THRESHOLDS:
5
thresholds = ALERT_THRESHOLDS[metric]
6
if 'min' in thresholds and value < thresholds['min']:
7
alerts.append(f"{metric}过低: {value} < {thresholds['min']}")
8
if 'max' in thresholds and value > thresholds['max']:
9
alerts.append(f"{metric}过高: {value} > {thresholds['max']}")
5.2 突变检测算法
采用CUSUM控制图检测数据突变:
PYTHON
4
def __init__(self, target, std_dev, threshold=5):
7
self.threshold = threshold
11
def update(self, value):
12
z = (value - self.target) / self.std_dev
13
self.cusum_pos = max(0, self.cusum_pos + z - 0.5)
14
self.cusum_neg = min(0, self.cusum_neg + z + 0.5)
16
if self.cusum_pos > self.threshold:
19
elif self.cusum_neg < -self.threshold:
初始化示例(用于温度突变检测):
PYTHON
1
temp_detector = CusumDetector(target=22, std_dev=0.5)
2
result = temp_detector.update(current_temp)
6. 系统部署与优化
6.1 服务化运行方案
使用systemd管理Python服务,创建/etc/systemd/system/environment_monitor.service:
INI
2
Description=Environment Monitoring Service
7
WorkingDirectory=/home/pi/smart_home
8
ExecStart=/usr/bin/python3 /home/pi/smart_home/main.py
12
WantedBy=multi-user.target
管理命令:
BASH
1
sudo systemctl daemon-reload
2
sudo systemctl enable environment_monitor
3
sudo systemctl start environment_monitor
6.2 性能优化实践
- 传感器采样优化:
- 将不同传感器的采样周期错开(如温湿度每30秒,PM2.5每2分钟)
- 使用多线程并行采集:
PYTHON
1
from threading import Thread
3
def read_sensor(sensor_func, result_dict, key):
4
result_dict[key] = sensor_func()
7
'temperature': sht31.read_temperature,
8
'pm25': pms5003.read_pm25
13
for name, func in sensors.items():
14
t = Thread(target=read_sensor, args=(func, results, name))
-
数据库写入优化:
- 采用批量写入代替单条插入
- 使用内存数据库作为缓冲区,定期持久化到磁盘
-
网络通信优化:
- WebSocket替代轮询实现实时更新
- 对历史数据启用Gzip压缩
7. 常见问题排查指南
7.1 传感器数据异常
症状:PM2.5读数持续为0或异常高值
- 检查UART接线是否松动
- 使用
screen /dev/ttyAMA0 9600直接查看原始数据
- 确认传感器风扇正常运转(PMS5003应有轻微震动)
症状:温湿度数据不更新
- 执行
i2cdetect -y 1确认设备地址
- 检查I2C是否被占用:
sudo lsof /dev/i2c-1
7.2 Web界面加载缓慢
优化方案:
- 减少初始加载数据量,改为按需加载
- 对ECharts启用数据采样:
JAVASCRIPT
4
progressiveThreshold: 5000
- 配置Nginx缓存静态资源:
NGINX
3
add_header Cache-Control "public";
7.3 误报警问题处理
调整策略:
- 设置报警延迟确认(连续3次超阈值才触发)
- 增加时间段豁免(如夜间不报温度低)
- 引入机器学习算法识别误报模式
PYTHON
1
from sklearn.ensemble import IsolationForest
3
clf = IsolationForest(contamination=0.01)
4
clf.fit(historical_data)
5
is_outlier = clf.predict([[current_value]]) == -1
8. 项目扩展方向
-
多房间组网监测:
- 使用MQTT协议实现分布式采集
- 每个房间部署ESP32作为边缘节点
-
智能联动控制:
- 当CO2超标时自动开启新风系统
- 温湿度异常时调节空调参数
-
能耗优化分析:
- 结合用电数据找出环境调节最佳策略
- 建立预测模型提前调整设备状态
-
语音交互集成:
- 对接智能音箱API实现语音查询
- 自定义预警播报内容
实现空调联动的示例代码:
PYTHON
3
def control_ac(device_id, mode, temp):
4
url = "https://api.smart-home.com/v1/devices/control"
6
"device_id": device_id,
12
headers = {"Authorization": "Bearer YOUR_TOKEN"}
13
response = requests.post(url, json=payload, headers=headers)
14
return response.json()
18
control_ac("ac_01", "cool", 26)