通过arduino采集室内室外温湿度数据,通过串口以发送json数据的形式,将采集到的数据传递给树莓派,树莓派中有一个服务,获取到来自串口的数据,将数据写入rrd文件。树莓派上再制作一个脚本,每5分钟生成图形,放在http服务器目录下,以便通过浏览器查看。
项目用到的材料清单如下
必备材料:
1、树莓派一块
2、欧西亚(Oregon Scientific)RTGR328N室外传感器一台。(欧西亚家用气象站的室外温度湿度传感器,后面会有简单的说明)
3、Arduino nano一块
4、RXB60 433MHz超外差接收模块一块(也使用过那种很便宜的超再生模块,似乎抗干扰能力很差,后来换用这种模块后就稳定多了)
5、连接线若干
6、433MHz弹簧天线一根(RXB60接收模块本身不含天线,需要自己在模块ANT针上连接一根天线)
7、170孔面包板两块(实际上这次使用两块170孔面包板比使用400孔面包板更为紧凑、空间利用率更高)
8、DHT11或者DHT22温湿度模块(主要用来获取室内温湿度,这里我用的是DHT22)
软件方面:
1、树莓派最好用debian或者ubuntu的系统
2、安装rrdtool和python3,
3、再装个nginx或者apache之类的http服务器,主要是为了通过网页查看rrdtool出的图。
为什么要使用欧西亚的温湿度传感器?
最初是为了利用家里现有的国产易美特气象站433MHz室外温湿度传感器来得到室外温湿度数据,也测试过另一款很便宜的标识为UPM的洋垃圾433Mhz温湿度传感器,发现对于我这个并不精通无线电这方面的门外汉来说,解码这些传感器数据困难很大,在研究过程中发现欧西亚气象站在外国使用量很大(因为欧西亚就是家用气象站的鼻祖嘛),研究的朋友也很多,有现成且很成熟的解码方案,遂在某鱼上低价入手一台二手的用来研究。有朋友会问了,为什么不用arduino、ESP8266\ESP32自己做一台蓝牙、WIFI或者433Mhz的室外传感器呢?我只是觉得像欧西亚这种成品传感器有它独特的优点:现成的可以在恶劣室外环境下工作的外壳(主要是保护主要器件防水、防尘、传感器有专门的开口)、433Mhz相对功耗比较低、使用干电池比较方便也能更好适应室外恶劣的环境,最后,最重要的一个有点就是拿来就用(我的这个传感器型号是RTGR328N,需要两节5号电池,有电波钟功能,只是频率和国内不同,需要改造才能使用)。
为什么不直接使用树莓派连接433Mhz接收器和DHT22传感器?
原因很简单,因为arduino有现成的欧西亚传感器解码代码,我们的原则是尽量拿来就用。
Arduino nano传感器连接:
天线部分,因为原来就是直接找了一根铜连接线插在面包板上,感觉总是拖个小尾巴,干脆就在某宝买了2.54转1.27的小电路板和弹簧天线,将天线焊在电路板1.27的孔内,2.54孔位焊上排针,做成了如下图的天线:
Arduino代码
欧西亚传感器解码部分来自于https://github.com/rene-d/oregon,在此基础上,我做了些修改,加入了读取DHT22传感器数据的代码,以及改为通过串口以json格式发送数据。
使用到的arduino库有ArduinoJson和DHT sensor library两个库。
// oregon.ino
// rene-d 01/2019
#include "DHT.h"
#include <ArduinoJson.h>
#include "Oregon.h"
#include "decode.h"
#define DHTPIN 3 // Digital pin connected to the DHT sensor
// Define pin where is 433Mhz receiver (here, pin 2)
#define MHZ_RECEIVER_PIN 2
//#define DHTTYPE DHT11 // DHT 11
#define DHTTYPE DHT22 // DHT 22 (AM2302), AM2321
//#define DHTTYPE DHT21 // DHT 21 (AM2301)
DHT dht(DHTPIN, DHTTYPE);
void setup()
{
Serial.begin(115200);
//DHTxx
dht.begin();
Serial.println(">>>>Arduino-Oregon RTGR328N Example<<<<");
//Serial.println("Setup started");
memset(&sensor_clock, 0, sizeof(sensor_clock));
sensor_clock.day = 1;
sensor_clock.month = 1;
pinMode(LED_BUILTIN, OUTPUT);
// Setup received data
attachInterrupt(digitalPinToInterrupt(MHZ_RECEIVER_PIN), ext_int_1, CHANGE);
//Serial.println("Setup completed");
//Serial.println("Begin receive 433MHz Oregon RTGR328N sensors...");
}
void loop()
{
//------------------------------------------
// Start process new data from Oregon sensors
//------------------------------------------
noInterrupts(); // Disable interrupts
word p = pulse;
pulse = 0;
interrupts(); // Enable interrupts
if (p != 0)
{
if (orscV2.nextPulse(p))
{
// Decode Hex Data once
byte length;
const byte *osdata = DataToDecoder(orscV2, length);
print_hexa(osdata, length);
// Serial.println(F("Outdoor:"));
oregon_decode(osdata, length);
float h = dht.readHumidity();
float t = dht.readTemperature();
if (isnan(h) || isnan(t)) {
Serial.println(F("Failed to read from DHT sensor!"));
return;
}
float hic = dht.computeHeatIndex(t, h, false);
// Serial.println(F("Indoor:"));
// Serial.print(F("Humidity: "));
// Serial.print(h);
// Serial.print(F("% Temperature: "));
// Serial.print(t);
// Serial.print(F("°C "));
// Serial.print(F("Heat index: "));
// Serial.print(hic);
// Serial.println(F("°C"));
StaticJsonDocument<200> doc;
doc["name"] = "RTGR328N";
JsonObject data = doc.createNestedObject("data");
JsonObject data_indoor = data.createNestedObject("indoor");
data_indoor["temperature"] = t;
data_indoor["humidity"] = h;
data_indoor["heat-index"] = hic;
JsonObject data_outdoor = data.createNestedObject("outdoor");
data_outdoor["humidity"] = sensor_info.hum;
data_outdoor["temperature"] = sensor_info.temp;
data_outdoor["channel"] = sensor_info.channel;
data_outdoor["id"] = sensor_info.id;
data_outdoor["time"] = date_time;
data_outdoor["bat"] = sensor_info.bat;
data_outdoor["RAW"] = raw_result;
serializeJson(doc, Serial);
//Serial.println();
//serializeJsonPretty(doc, Serial);
}
}
update_clock();
if (Serial.available() > 0)
{
// read the incoming byte:
byte incomingByte = Serial.read();
if (incomingByte == 't')
{
// print the current time
print_hexa(&incomingByte, 0);
}
}
}
int days_in_month(byte month, byte year)
{
static byte days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if (month == 2)
{
// we will never reach 2100
if (year % 4 == 0)
return 29;
}
return days[month];
}
void update_clock()
{
unsigned long now = millis();
unsigned long o = now - sensor_clock.now;
if (o >= 1000)
{
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
sensor_clock.now = now;
while (o >= 1000)
{
// advance the clock by one second
o -= 1000;
// dirty code done dirt cheap...
sensor_clock.second++;
if (sensor_clock.second >= 60)
{
sensor_clock.second = 0;
sensor_clock.minute++;
if (sensor_clock.minute >= 60)
{
sensor_clock.minute = 0;
sensor_clock.hour++;
if (sensor_clock.hour >= 24)
{
sensor_clock.hour = 0;
sensor_clock.day++;
if (sensor_clock.day > days_in_month(sensor_clock.month, sensor_clock.year))
{
sensor_clock.day = 1;
sensor_clock.month++;
if (sensor_clock.month > 12)
{
sensor_clock.month = 1;
sensor_clock.year++;
}
}
}
}
}
}
}
}
void print_hexa(const byte *data, byte length)
{
raw_result = "";
//Serial.println();
// print the sensor/board clock
char buf[32];
snprintf(buf, 32, "%04u/%02u/%02u %02u:%02u:%02u.%03lu",
2000 + sensor_clock.year, sensor_clock.month, sensor_clock.day,
sensor_clock.hour, sensor_clock.minute, sensor_clock.second,
millis() - sensor_clock.now);
date_time = buf;
//Serial.println(buf);
// print data in byte order, that's NOT the nibble order
//for (byte i = 0; i < length; ++i)
//{
// Serial.print(data[i] >> 4, HEX);
// Serial.print(data[i] & 0x0F, HEX);
//}
//Serial.println();
for (byte i = 0; i < length; ++i)
{
raw_result += tohex(data[i] >> 4);
raw_result += tohex(data[i] & 0x0F);
}
}
decode.h文件主要是修改了原来直接输出到串口的部分代码,变更为将数据存储在全局变量中,以便最后统一输出为json格式。
// decode.h
// rene-d 01/2019
// internal clock, set from OS sensor
struct
{
byte year, month, day;
byte hour, minute, second;
unsigned long now;
} sensor_clock;
struct
{
byte id, channel;
float temp;
byte hum, bat;
} sensor_info;
String raw_result = "";
String date_time = "";
//
// get a nibble (a half byte)
//
byte nibble(const byte *osdata, byte n)
{
if (n & 1)
{
return osdata[n / 2] >> 4;
}
else
{
return osdata[n / 2] & 0xf;
}
}
//
// calculate the packet checksum (sum of all nibbles)
//
bool checksum(const byte *osdata, byte last, byte check)
{
byte calc = 0;
for (byte i = 1; i <= last; i++)
{
calc += nibble(osdata, i);
}
return (check == calc);
}
//
// print a buffer in nibble order, with nibbles packed by field
//
void print_nibbles(const byte *osdata, size_t len, const char *def)
{
static const char digits[] = "0123456789ABCDEF";
char hexa[128];
size_t i = 0;
size_t j = 0;
size_t k = 0;
char c, n;
n = def[0];
if (n)
n -= '0';
c = n;
while (i < len * 2 && j < sizeof(hexa) - 3)
{
hexa[j++] = digits[nibble(osdata, i++)];
if (c > 0)
{
c--;
if (c == 0)
{
// reverse last def[k] chars
if (n > 1)
{
for (char z = 0; z < n / 2; ++z)
{
c = hexa[j - 1 - z];
hexa[j - 1 - z] = hexa[j - n + z];
hexa[j - n + z] = c;
}
}
hexa[j++] = ' ';
n = def[++k];
if (n)
n -= '0';
c = n;
}
}
}
hexa[j] = 0;
Serial.print("data: ");
Serial.println(hexa);
}
//
// message 3EA8 or 3EC8 : clock
//
void decode_date_time(const byte *osdata, size_t len)
{
if (len < 12)
return;
byte crc = osdata[11];
bool ok = checksum(osdata, 21, crc);
byte channel = (osdata[2] >> 4);
byte rolling_code = osdata[3];
int year = (osdata[9] >> 4) + 10 * (osdata[10] & 0xf);
int month = (osdata[8] >> 4);
int day = (osdata[7] >> 4) + 10 * (osdata[8] & 0xf);
int hour = (osdata[6] >> 4) + 10 * (osdata[7] & 0xf);
int minute = (osdata[5] >> 4) + 10 * (osdata[6] & 0xf);
int second = (osdata[4] >> 4) + 10 * (osdata[5] & 0xf);
channel = nibble(osdata, 5);
#ifdef ARDUINO
if (!ok)
Serial.println(" bad crc");
if (ok && (nibble(osdata, 8) & 2) != 0)
{
// update the sensor clock
sensor_clock.now = millis();
sensor_clock.year = year;
sensor_clock.month = month;
sensor_clock.day = day;
sensor_clock.hour = hour;
sensor_clock.minute = minute;
sensor_clock.second = second;
Serial.println("update sensor clock!");
}
//Serial.print(" id: ");
//Serial.println(rolling_code);
//Serial.print(" channel: ");
//Serial.println(channel);
char buf[100];
snprintf(buf, sizeof(buf), " date: 20%02d/%02d/%02d", year, month, day);
//Serial.println(buf);
snprintf(buf, sizeof(buf), " time: %02d:%02d:%02d", hour, minute, second);
//Serial.println(buf);
#else
print_nibbles(osdata, len, "14121222211212");
char buf[80];
snprintf(buf, sizeof(buf), "channel=%d crc=$%02X %s id=%d channel=%d state=%d clock=20%02d/%02d/%02d %02d:%02d:%02d",
channel, crc, ok ? "OK" : "KO", rolling_code,
channel, nibble(osdata, 8),
year, month, day, hour, minute, second);
Serial.println(buf);
static const char *label[] = {
"alwaysA", // 0 b0 A=1010 - not part of the message
"id_msg", // 1 b0
"id_msg", // 2 b1
"id_msg", // 3 b1
"id_msg", // 4 b2
"channel", // 5 b2
"rolling code", // 6 b3
"rolling code", // 7 b3
"clock state", // 8 b4 0,4,8: date is invalid, 2 or 6: date is valid
"second (units)", // 9 b4
"second (tens)", // 10 b5
"minute (unit)", // 11 b5
"minute (tens)", // 12 b6
"hour (units)", // 13 b6
"hour (tens)", // 14 b7
"day (units)", // 15 b7
"day (tens)", // 16 b8
"month", // 17 b8
"day of week", // 18 b9
"year (units)", // 19 b9
"year (tens)", // 20 b10
"?", // 21 b10
"crc", // 22 b11
"crc", // 23 b11
};
for (int i = 0; i < 24; ++i)
{
snprintf(buf, sizeof(buf), " nibble %2d : %X %s", i, nibble(osdata, i), label[i]);
Serial.println(buf);
}
#endif
}
//
// message 3CCx or 02D1: temperature humidity
//
void decode_temp_hygro(const byte *osdata, size_t len)
{
if (len < 9)
return;
byte crc = osdata[8];
bool ok = checksum(osdata, 15, crc); // checksum = nibbles 1-15, result is nibbles 17..16
byte channel = (osdata[2] >> 4);
byte rolling_code = osdata[3];
int temp = ((osdata[5] >> 4) * 100) + ((osdata[5] & 0x0F) * 10) + ((osdata[4] >> 4));
if (osdata[6] & 0x08)
temp = -temp;
byte hum = ((osdata[7] & 0x0F) * 10) + (osdata[6] >> 4);
//
byte BatteryLevel = (osdata[4] & 0x4) ? 10 : 90;
// Serial.println("Oregon battery level: " + String(BatteryLevel));
//
byte bat = osdata[4] & 0x4;
// byte bat = osdata[4] & 0x0F;
#ifdef ARDUINO
if (!ok)
Serial.println(" bad crc");
//Serial.print(" id: [");
//Serial.print(rolling_code);
//Serial.print("] channel: [");
//Serial.print(channel);
//Serial.print("] temperature: [");
//Serial.print(temp / 10.);
//Serial.print("] humidity: [");
//Serial.print(hum);
//Serial.print("] bat: [");
//Serial.print(BatteryLevel);
// Serial.print(bat);
//if ((bat & 4) != 0)
//{
// Serial.println(" low]");
//}
//else
//{
// Serial.println(" ok]");
//}
sensor_info.id = rolling_code;
sensor_info.channel = channel;
//sensor_info.temp = temp;
sensor_info.temp = temp / 10.;
sensor_info.hum = hum;
sensor_info.bat = BatteryLevel;
#else
print_nibbles(osdata, len, "141214212");
char buf[80];
snprintf(buf, sizeof(buf), "channel=%d crc=$%02X %s id=%d temp=%.1lf°C hum=%d%% bat=%d",
channel, crc, ok ? "OK" : "KO", rolling_code,
temp / 10., hum, bat);
Serial.println(buf);
static const char *label[] = {
"alwaysA", // 0 b0 lsb always A=1010
"id_msg", // 1 b0 msb
"id_msg", // 2 b1 lsb
"id_msg", // 3 b1 msb
"id_msg", // 4 b2 lsb
"channel", // 5 b2 msb
"rolling code", // 6 b3 lsb
"rolling code", // 7 b3 msb
"battery", // 8 b4 lsb bit 3=1 => low?
"temperature (tenths)", // 9 b4 msb
"temperature (units)", // 10 b5 lsb
"temperature (tens)", // 11 b5 msb
"temperature (sign)", // 12 b6 lsb
"humidity (units)", // 13 b6 msb
"humidity (tens)", // 14 b7 lsb
"comfort", // 15 b7 msb comfort ??? (according to RFLink) 0: normal, 4: comfortable, 8: dry, C: wet
"crc", // 16 b8 lsb
"crc", // 17 b8 msb
};
for (int i = 0; i < 18; ++i)
{
snprintf(buf, sizeof(buf), " nibble %2d : %X %s", i, nibble(osdata, i), label[i]);
Serial.println(buf);
}
#endif
}
void oregon_decode(const byte *osdata, size_t len)
{
// we need 2 bytes at least
// - the preamble A
// - the ID on four nibbles
if (len < 3)
return;
uint16_t id = (((uint16_t)nibble(osdata, 4)) << 12) +
(((uint16_t)nibble(osdata, 3)) << 8) +
(((uint16_t)nibble(osdata, 2)) << 4) +
(((uint16_t)nibble(osdata, 1)));
#ifndef ARDUINO
char buf[32];
snprintf(buf, 32, "message: %04X len=%zu", id, len);
Serial.println(buf);
#endif
if ((id & 0xFFF0) == 0x3CC0 || id == 0x02D1)
{
decode_temp_hygro(osdata, len);
}
else if (id == 0x3EA8 || id == 0x3EC8)
{
decode_date_time(osdata, len);
}
else
{
#ifndef ARDUINO
print_nibbles(osdata, len, "1412");
Serial.println("UNKNOWN");
#endif
}
}
String tohex(int n) {
if (n > 15) {
return "0";
}
String result = "";
char _16[] = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
};
result = _16[n];
return result;
}
树莓派上首先要自己写一个服务,在后台读取USB 串口传过来的json数据,写入rrd文件。
首先,在/home/ubuntu/目录下新建service_read_sensor.py文件,内容如下,主要作用是
service文件调用的
#!/usr/bin/python3
import serial
import threading
import json
import time
import math
import rrdtool
import sys
DATA = "" # 读取的数据
NOEND = True # 是否读取结束
sensor_data = ""
def calc_heat_index(T, RH):
'''NOAA计算体感温度 参数为气温(摄氏度)和相对湿度(0~100或者0~1)'''
if RH < 1:
RH *= 100
T = 1.8 * T + 32
HI = 0.5 * (T + 61 + (T - 68) * 1.2 + RH * 0.094)
if HI >= 80: # 如果不小于 80华氏度 则用完整公式重新计算
HI = -42.379 + 2.04901523 * T + 10.14333127 * RH - .22475541 * T * RH \
- .00683783 * T * T - .05481717 * RH * RH + .00122874 * T * T * RH \
+ .00085282 * T * RH * RH - .00000199 * T * T * RH * RH
if RH < 13 and 80 < T < 112:
ADJUSTMENT = (13 - RH) / 4 * math.sqrt((17 - abs(T - 95)) / 17)
HI -= ADJUSTMENT
elif RH > 85 and 80 < T < 87:
ADJUSTMENT = (RH - 85) * (87 - T) / 50
HI += ADJUSTMENT
return round((HI - 32) / 1.8, 2)
# 读数据的本体
def read_data(ser):
#global DATA, NOEND, sensor_data
global DATA, NOEND, sensor_data,logfile
# 循环接收数据(此为死循环,可用线程实现)
while NOEND:
if ser.in_waiting:
#logfile = open("\tmp\read_serial.log", "a")
#DATA = ser.read(ser.in_waiting).decode("gbk")
DATA = ser.readline().decode("gbk")
logfile.write("\n["+time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())+"]receive:"+DATA+"\n")
logfile.write("\n["+time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())+"]json.loads:")
try:
sensor_data = json.loads(DATA)
except ValueError:
logfile.write("error for json\n")
else:
logfile.write(str(sensor_data)+"\n")
update=rrdtool.updatev('/home/ubuntu/test2.rrd','N:%s:%s:%s:%s:%s:%s' % (str(sensor_data['data']['indoor']['temperature']),str(sensor_data['data']['indoor']['humidity']),str(sensor_data['data']['indoor']['heat-index']),str(sensor_data['data']['outdoor']['temperature']),str(sensor_data['data']['outdoor']['humidity']),str(calc_heat_index(sensor_data['data']['outdoor']['temperature'],sensor_data['data']['outdoor']['humidity']))))
logfile.write("\n["+time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())+"]rrdtool update:"+str(update)+"\n")
logfile.flush()
#logfile.close()
# 打开串口
def open_seri(portx, bps, timeout):
global logfile
ret = False
try:
# 打开串口,并得到串口对象
ser = serial.Serial(portx, bps, timeout=timeout)
# 判断是否成功打开
if(ser.is_open):
ret = True
th = threading.Thread(target=read_data, args=(ser,)) # 创建一个子线程去等待读数据
th.start()
except Exception as e:
#logfile = open("\tmp\open_serial.log", "a")
logfile.write("\n["+time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())+"]open serial error:"+e+"\n")
logfile.flush()
#logfile.close()
return ser, ret
# 关闭串口
def close_seri(ser):
global NOEND
NOEND = False
ser.close()
# 写数据
def write_to_seri(ser, text):
res = ser.write(text.encode("gbk")) # 写
return res
# 读数据
def read_from_seri():
global DATA
data = DATA
DATA = "" #清空当次读取
return data
if __name__ == "__main__":
logfile = open("/tmp/read_serial.log", "a")
ser, ret = open_seri('/dev/ttyUSB0', 115200, 1)
while True:
time.sleep(2)
logfile.close()
在/lib/systemd/system目录下新建read_serial_sensor.service文件,内容如下:
[Unit]
Description=Read Serial Sensor Service
After=multi-user.target
Conflicts=getty@tty1.service
[Service]
Type=simple
ExecStart=/usr/bin/python3 /home/ubuntu/service_read_sensor.py
StandardInput=tty-force
[Install]
WantedBy=multi-user.target
sudo systemctl start read_serial_sensor.service
sudo systemctl status read_serial_sensor.service
sudo systemctl enable read_serial_sensor.service
另外再写一个定时执行的脚本,每5分钟生成一次rrd图,图片放在/usr/local/nginx/html/images/目录下,这是我的nginx根目录下的一个图片目录。
#!/usr/bin/env python3
import sys
import time
import rrdtool
rrdtool.graph(
'/usr/local/nginx/html/images/temp_test-1day.png',
'--title','室内室外温度与体感温度24小时曲线图',
'--vertical-label','temp °C',
'--alt-autoscale-max',
'--slope-mode',
'--watermark',time.strftime("%a %b %d %H:%M:%S %Y", time.localtime()),
'--lower-limit','-10',
'--font','TITLE:10:',
'--font','AXIS:7:',
'--font','LEGEND:8:',
'--font','UNIT:7:',
'--font','WATERMARK:7:',
'DEF:a=/home/ubuntu/test2.rrd:in_temp:AVERAGE',
'DEF:b=/home/ubuntu/test2.rrd:out_temp:AVERAGE',
'DEF:c=/home/ubuntu/test2.rrd:in_hic:AVERAGE',
'DEF:d=/home/ubuntu/test2.rrd:out_hic:AVERAGE',
'LINE1:a#1E90FFFF:室内温度',
'GPRINT:a:LAST: Current\:%2.2lf°C',
'GPRINT:a:AVERAGE:Average\:%2.2lf°C',
'GPRINT:a:MAX:Maximum\:%2.2lf°C',
'LINE1:c#87CEEBFF:室内体感',
'GPRINT:c:LAST: Current\:%2.2lf°C',
'GPRINT:c:AVERAGE:Average\:%2.2lf°C',
'GPRINT:c:MAX:Maximum\:%2.2lf°C',
'LINE1:b#00FF7FFF:室外温度',
'GPRINT:b:LAST:Current\:%2.2lf°C',
'GPRINT:b:AVERAGE:Average\:%2.2lf°C',
'GPRINT:b:MAX:Maximum\:%2.2lf°C',
'LINE1:d#66CD00FF:室外体感',
'GPRINT:d:LAST:Current\:%2.2lf°C',
'GPRINT:d:AVERAGE:Average\:%2.2lf°C',
'GPRINT:d:MAX:Maximum\:%2.2lf°C')
rrdtool.graph(
'/usr/local/nginx/html/images/temp_test-1week.png',
'--start','end-1w',
'--end','00:00',
'--title','室内室外温度',
'--vertical-label','temp °C',
'--alt-autoscale-max',
'--slope-mode',
'--watermark',time.strftime("%a %b %d %H:%M:%S %Y", time.localtime()),
'--lower-limit','-10',
'--font','TITLE:10:',
'--font','AXIS:7:',
'--font','LEGEND:8:',
'--font','UNIT:7:',
'--font','WATERMARK:7:',
'DEF:a=/home/ubuntu/test2.rrd:in_temp:AVERAGE',
'DEF:b=/home/ubuntu/test2.rrd:out_temp:AVERAGE',
'LINE1:a#00CF00FF:indoor temp',
'GPRINT:a:LAST: Current\:%2.2lf°C',
'GPRINT:a:AVERAGE:Average\:%2.2lf°C',
'GPRINT:a:MAX:Maximum\:%2.2lf°C\\n',
'LINE1:b#002A97FF:outdoor temp',
'GPRINT:b:LAST:Current\:%2.2lf°C',
'GPRINT:b:AVERAGE:Average\:%2.2lf°C',
'GPRINT:b:MAX:Maximum\:%2.2lf°C')
rrdtool.graph(
'/usr/local/nginx/html/images/temp_test-1month.png',
'--start','end-1m',
'--end','00:00',
'--title','室内室外温度',
'--vertical-label','temp °C',
'--alt-autoscale-max',
'--slope-mode',
'--watermark',time.strftime("%a %b %d %H:%M:%S %Y", time.localtime()),
'--lower-limit','-10',
'--font','TITLE:10:',
'--font','AXIS:7:',
'--font','LEGEND:8:',
'--font','UNIT:7:',
'--font','WATERMARK:7:',
'DEF:a=/home/ubuntu/test2.rrd:in_temp:AVERAGE',
'DEF:b=/home/ubuntu/test2.rrd:out_temp:AVERAGE',
'LINE1:a#00CF00FF:indoor temp',
'GPRINT:a:LAST: Current\:%2.2lf°C',
'GPRINT:a:AVERAGE:Average\:%2.2lf°C',
'GPRINT:a:MAX:Maximum\:%2.2lf°C\\n',
'LINE1:b#002A97FF:outdoor temp',
'GPRINT:b:LAST:Current\:%2.2lf°C',
'GPRINT:b:AVERAGE:Average\:%2.2lf°C',
'GPRINT:b:MAX:Maximum\:%2.2lf°C')
rrdtool.graph(
'/usr/local/nginx/html/images/temp_test-1year.png',
'--start','end-1y',
'--end','00:00',
'--title','室内室外温度',
'--vertical-label','temp °C',
'--alt-autoscale-max',
'--slope-mode',
'--watermark',time.strftime("%a %b %d %H:%M:%S %Y", time.localtime()),
'--lower-limit','-10',
'--font','TITLE:10:',
'--font','AXIS:7:',
'--font','LEGEND:8:',
'--font','UNIT:7:',
'--font','WATERMARK:7:',
'DEF:a=/home/ubuntu/test2.rrd:in_temp:AVERAGE',
'DEF:b=/home/ubuntu/test2.rrd:out_temp:AVERAGE',
'LINE1:a#00CF00FF:indoor temp',
'GPRINT:a:LAST: Current\:%2.2lf°C',
'GPRINT:a:AVERAGE:Average\:%2.2lf°C',
'GPRINT:a:MAX:Maximum\:%2.2lf°C\\n',
'LINE1:b#002A97FF:outdoor temp',
'GPRINT:b:LAST:Current\:%2.2lf°C',
'GPRINT:b:AVERAGE:Average\:%2.2lf°C',
'GPRINT:b:MAX:Maximum\:%2.2lf°C')
rrdtool.graph(
'/usr/local/nginx/html/images/hum_test-1day.png',
'--title','室内室外湿度',
'--vertical-label','hum %',
'--alt-autoscale-max',
'--slope-mode',
'--watermark',time.strftime("%a %b %d %H:%M:%S %Y", time.localtime()),
'--lower-limit','0',
'--font','TITLE:10:',
'--font','AXIS:7:',
'--font','LEGEND:8:',
'--font','UNIT:7:',
'--font','WATERMARK:7:',
'DEF:a=/home/ubuntu/test2.rrd:in_hum:AVERAGE',
'DEF:b=/home/ubuntu/test2.rrd:out_hum:AVERAGE',
'LINE1:a#00CF00FF:indoor hum',
'GPRINT:a:LAST: Current\:%3.2lf%%',
'GPRINT:a:AVERAGE:Average\:%3.2lf%%',
'GPRINT:a:MAX:Maximum\:%3.2lf%%\\n',
'LINE1:b#002A97FF:outdoor hum',
'GPRINT:b:LAST:Current\:%3.2lf%%',
'GPRINT:b:AVERAGE:Average\:%3.2lf%%',
'GPRINT:b:MAX:Maximum\:%3.2lf%%')
rrdtool.graph(
'/usr/local/nginx/html/images/hum_test-1week.png',
'--start','end-1w',
'--end','00:00',
'--title','室内室外湿度',
'--vertical-label','hum %',
'--alt-autoscale-max',
'--slope-mode',
'--watermark',time.strftime("%a %b %d %H:%M:%S %Y", time.localtime()),
'--lower-limit','0',
'--font','TITLE:10:',
'--font','AXIS:7:',
'--font','LEGEND:8:',
'--font','UNIT:7:',
'--font','WATERMARK:7:',
'DEF:a=/home/ubuntu/test2.rrd:in_hum:AVERAGE',
'DEF:b=/home/ubuntu/test2.rrd:out_hum:AVERAGE',
'LINE1:a#00CF00FF:indoor hum',
'GPRINT:a:LAST: Current\:%3.2lf%%',
'GPRINT:a:AVERAGE:Average\:%3.2lf%%',
'GPRINT:a:MAX:Maximum\:%3.2lf%%\\n',
'LINE1:b#002A97FF:outdoor hum',
'GPRINT:b:LAST:Current\:%3.2lf%%',
'GPRINT:b:AVERAGE:Average\:%3.2lf%%',
'GPRINT:b:MAX:Maximum\:%3.2lf%%')
rrdtool.graph(
'/usr/local/nginx/html/images/hum_test-1month.png',
'--start','end-1m',
'--end','00:00',
'--title','室内室外湿度',
'--vertical-label','hum %',
'--alt-autoscale-max',
'--slope-mode',
'--watermark',time.strftime("%a %b %d %H:%M:%S %Y", time.localtime()),
'--lower-limit','0',
'--font','TITLE:10:',
'--font','AXIS:7:',
'--font','LEGEND:8:',
'--font','UNIT:7:',
'--font','WATERMARK:7:',
'DEF:a=/home/ubuntu/test2.rrd:in_hum:AVERAGE',
'DEF:b=/home/ubuntu/test2.rrd:out_hum:AVERAGE',
'LINE1:a#00CF00FF:indoor hum',
'GPRINT:a:LAST: Current\:%3.2lf%%',
'GPRINT:a:AVERAGE:Average\:%3.2lf%%',
'GPRINT:a:MAX:Maximum\:%3.2lf%%\\n',
'LINE1:b#002A97FF:outdoor hum',
'GPRINT:b:LAST:Current\:%3.2lf%%',
'GPRINT:b:AVERAGE:Average\:%3.2lf%%',
'GPRINT:b:MAX:Maximum\:%3.2lf%%')
rrdtool.graph(
'/usr/local/nginx/html/images/hum_test-1year.png',
'--start','end-1y',
'--end','00:00',
'--title','室内室外湿度',
'--vertical-label','hum %',
'--alt-autoscale-max',
'--slope-mode',
'--watermark',time.strftime("%a %b %d %H:%M:%S %Y", time.localtime()),
'--lower-limit','0',
'--font','TITLE:10:',
'--font','AXIS:7:',
'--font','LEGEND:8:',
'--font','UNIT:7:',
'--font','WATERMARK:7:',
'DEF:a=/home/ubuntu/test2.rrd:in_hum:AVERAGE',
'DEF:b=/home/ubuntu/test2.rrd:out_hum:AVERAGE',
'LINE1:a#00CF00FF:indoor hum',
'GPRINT:a:LAST: Current\:%3.2lf%%',
'GPRINT:a:AVERAGE:Average\:%3.2lf%%',
'GPRINT:a:MAX:Maximum\:%3.2lf%%\\n',
'LINE1:b#002A97FF:outdoor hum',
'GPRINT:b:LAST:Current\:%3.2lf%%',
'GPRINT:b:AVERAGE:Average\:%3.2lf%%',
'GPRINT:b:MAX:Maximum\:%3.2lf%%')
rrdtool.graph(
'/usr/local/nginx/html/images/hic_test-1day.png',
'--title','室内室外体感温度',
'--vertical-label','hic °C',
'--alt-autoscale-max',
'--slope-mode',
'--watermark',time.strftime("%a %b %d %H:%M:%S %Y", time.localtime()),
'--lower-limit','-10',
'--font','TITLE:10:',
'--font','AXIS:7:',
'--font','LEGEND:8:',
'--font','UNIT:7:',
'--font','WATERMARK:7:',
'DEF:a=/home/ubuntu/test2.rrd:in_hic:AVERAGE',
'DEF:b=/home/ubuntu/test2.rrd:out_hic:AVERAGE',
'LINE1:a#00CF00FF:indoor hic',
'GPRINT:a:LAST: Current\:%2.2lf°C',
'GPRINT:a:AVERAGE:Average\:%2.2lf°C',
'GPRINT:a:MAX:Maximum\:%2.2lf°C\\n',
'LINE1:b#002A97FF:outdoor hic',
'GPRINT:b:LAST:Current\:%2.2lf°C',
'GPRINT:b:AVERAGE:Average\:%2.2lf°C',
'GPRINT:b:MAX:Maximum\:%2.2lf°C')
rrdtool.graph(
'/usr/local/nginx/html/images/hic_test-1week.png',
'--start','end-1w',
'--end','00:00',
'--title','室内室外体感温度',
'--vertical-label','hic °C',
'--alt-autoscale-max',
'--slope-mode',
'--watermark',time.strftime("%a %b %d %H:%M:%S %Y", time.localtime()),
'--lower-limit','-10',
'--font','TITLE:10:',
'--font','AXIS:7:',
'--font','LEGEND:8:',
'--font','UNIT:7:',
'--font','WATERMARK:7:',
'DEF:a=/home/ubuntu/test2.rrd:in_hic:AVERAGE',
'DEF:b=/home/ubuntu/test2.rrd:out_hic:AVERAGE',
'LINE1:a#00CF00FF:indoor hic',
'GPRINT:a:LAST: Current\:%2.2lf°C',
'GPRINT:a:AVERAGE:Average\:%2.2lf°C',
'GPRINT:a:MAX:Maximum\:%2.2lf°C\\n',
'LINE1:b#002A97FF:outdoor hic',
'GPRINT:b:LAST:Current\:%2.2lf°C',
'GPRINT:b:AVERAGE:Average\:%2.2lf°C',
'GPRINT:b:MAX:Maximum\:%2.2lf°C')
rrdtool.graph(
'/usr/local/nginx/html/images/hic_test-1month.png',
'--start','end-1m',
'--end','00:00',
'--title','室内室外体感温度',
'--vertical-label','hic °C',
'--alt-autoscale-max',
'--slope-mode',
'--watermark',time.strftime("%a %b %d %H:%M:%S %Y", time.localtime()),
'--lower-limit','-10',
'--font','TITLE:10:',
'--font','AXIS:7:',
'--font','LEGEND:8:',
'--font','UNIT:7:',
'--font','WATERMARK:7:',
'DEF:a=/home/ubuntu/test2.rrd:in_hic:AVERAGE',
'DEF:b=/home/ubuntu/test2.rrd:out_hic:AVERAGE',
'LINE1:a#00CF00FF:indoor hic',
'GPRINT:a:LAST: Current\:%2.2lf°C',
'GPRINT:a:AVERAGE:Average\:%2.2lf°C',
'GPRINT:a:MAX:Maximum\:%2.2lf°C\\n',
'LINE1:b#002A97FF:outdoor hic',
'GPRINT:b:LAST:Current\:%2.2lf°C',
'GPRINT:b:AVERAGE:Average\:%2.2lf°C',
'GPRINT:b:MAX:Maximum\:%2.2lf°C')
rrdtool.graph(
'/usr/local/nginx/html/images/hic_test-1year.png',
'--start','end-1y',
'--end','00:00',
'--title','室内室外体感温度',
'--vertical-label','hic °C',
'--alt-autoscale-max',
'--slope-mode',
'--watermark',time.strftime("%a %b %d %H:%M:%S %Y", time.localtime()),
'--lower-limit','-10',
'--font','TITLE:10:',
'--font','AXIS:7:',
'--font','LEGEND:8:',
'--font','UNIT:7:',
'--font','WATERMARK:7:',
'DEF:a=/home/ubuntu/test2.rrd:in_hic:AVERAGE',
'DEF:b=/home/ubuntu/test2.rrd:out_hic:AVERAGE',
'LINE1:a#00CF00FF:indoor hic',
'GPRINT:a:LAST: Current\:%2.2lf°C',
'GPRINT:a:AVERAGE:Average\:%2.2lf°C',
'GPRINT:a:MAX:Maximum\:%2.2lf°C\\n',
'LINE1:b#002A97FF:outdoor hic',
'GPRINT:b:LAST:Current\:%2.2lf°C',
'GPRINT:b:AVERAGE:Average\:%2.2lf°C',
'GPRINT:b:MAX:Maximum\:%2.2lf°C')
在系统cron中添加一个任务,每5分钟执行一次脚本,生成rrd图。
*/5 * * * * sudo python3 /home/ubuntu/rrdtool_graph.py >> /tmp/temp_hum_rrdtool_graph.log 2>&1
在nginx的html根目录下新建index.html文件,将生成的rrd图放到html页面中,这样就可以通过网页查看曲线图了。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<meta http-equiv="Expires" content="0">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-control" content="no-cache">
<meta http-equiv="Cache" content="no-cache">
<meta http-equiv="refresh" content="300">
<title>温度湿度监控</title>
</head>
<body>
<table>
<tr>
<td>
<img src="images/temp_test-1day.png" alt="1day">
1day
</br>
<img src="images/temp_test-1week.png" alt="1week">
1week
</br>
<img src="images/temp_test-1month.png" alt="1month">
1month
</br>
<img src="images/temp_test-1year.png" alt="1year">
1year
</td>
<td>
<img src="images/hum_test-1day.png" alt="1day">
1day
</br>
<img src="images/hum_test-1week.png" alt="1week">
1week
</br>
<img src="images/hum_test-1month.png" alt="1month">
1month
</br>
<img src="images/hum_test-1year.png" alt="1year">
1year
</td>
<td>
<img src="images/hic_test-1day.png" alt="1day">
1day
</br>
<img src="images/hic_test-1week.png" alt="1week">
1week
</br>
<img src="images/hic_test-1month.png" alt="1month">
1month
</br>
<img src="images/hic_test-1year.png" alt="1year">
1year
</td>
</tr>
</table>
</body>
</html>
节目预告:
下一篇文章我们将把这个系统进行改造,使用博世bme280传感器来代替DHT22传感器,通过bme280传感器我们也可以得到大气压数据。开发板改为ESP8266,将原来串口输出的数据改为wifi连接,将数据以mqtt形式发布到mqtt broker,使用domoticz来订阅这个主题,以达到将传感器数据传入domoticz的目的。