外面的天气真可怕(或者真的吗?)

严寒的天气正在逼近,这应该会激起一种本能的愿望,让你整理好你的房子并进行监控,以防止水管冻结和爆裂。因此,让我们及时了解一个项目,该项目在各个区域设置了一些温度探头,读取它们并在自定义仪表板中报告。真正的家庭自动化专家会更进一步,设置继电器来打开加热带,甚至可能是一些执行器来控制水流。也许明年吧,但目前,我们只想能够监控我们家(在本例中是蒙大拿州的一个小木屋)的重要区域,并了解一段时间内的温度模式,以便更好地规划。与大多数项目一样,解决方案不止一种,尤其是在它取决于你手头有什么的情况下。当时,手头的东西清单如下:

  • 4 个温度传感器(热电偶)。

  • 1 个 RS-232 热电偶模块,特别是 DGH D5331

  • 1 个 TS-7500 单板计算机,配备 TS-752 和外壳。

  • 1 个 USB 拇指驱动器用于存储。

  • 各种接线,包括以太网。

  • 1 个 DB9 公头转 RJ45 母头适配器。

  • 1 个 Web 服务器(桌面、虚拟机或基于云)。

如果您有兴趣做类似的事情,我建议您研究使用任何使用 DS18B20 IC(或类似物)的温度传感器,并使用带有 Modbus 的 TS-1700 温度传感器模块和 TS-7680 单板计算机。您可以省一些钱,并将可以使用的温度传感器数量增加一倍。

总的来说,下图显示了我们想要构建的内容。

四个温度传感器连接到一个热电偶模块,该模块使用充当 SFTP 文件服务器的单板计算机读取。位于不同位置的 Web 服务器能够访问文件服务器,以将其温度数据存储在自己的数据库中,并向最终用户呈现一个美观的仪表板。本项目指南将略过硬件设置,重点介绍一个通过 RS-232 读取温度数据的 Python 脚本,并花一些时间使用 Google Charts 设置 HTML 仪表板。希望它能激发您的下一个项目。

设置硬件

首先要考虑的事项之一是温度传感器应该位于何处,以便提供最有价值的数据。在我们的例子中,在一个偏远的小木屋里,我们想监测室内和室外温度,以及管道所在的地板下和最冷的浴室。

一旦温度探头安装在这些关键位置,电线就被拉进一个壁橱,RS-232 热电偶模块和 TS-7500 就安装在那里。这里的接线非常简单,尤其是在查阅手册时,因此在这里详细介绍没有太多价值。我们使用了一些 RJ45 电线和插孔,使事情尽可能简洁,并使用 DB9 转 RJ45 适配器将我们的热电偶模块连接到 TS-7500 的 RS-232 端口。

您可能需要记住的另一个考虑因素是电源和互联网连接的可靠性。这个项目是在一个或多或少脱离电网且非常容易发生电源和互联网中断的偏远地区设置的。因此,我们将所有东西都连接到电池备用电源。不仅如此,考虑到 Linux 和电源中断的性质,我们还将 TS-7500 设置为从 只读文件系统 启动(在设置好我们的软件之后)。这就是我们使用 USB 拇指驱动器来存储我们定期收集的温度数据的原因。从技术上讲,我们可以在需要时轮询温度,然后将它们直接存储到我们的网站数据库中,但那样我们可能会错过收集历史数据。最后一个考虑因素是确保在我们的路由器上设置 SSH(端口 22)的端口转发,然后使用像 dyndns.org 这样的服务,以便从外部世界轻松访问此服务器。

设置软件

下一步是想出一个脚本来读取来自 RS-232 端口的温度数据。Python 非常适合这种事情,所以这是选择的解决方案。该脚本打开 RS-232 端口(使用 xuartctl),向热电偶模块发送读取命令 (RD),读取结果,转换它,然后将其保存到 USB 拇指驱动器上的 CSV 格式文件中。这是该脚本最终的样子:


get-temps.py


#!/usr/bin/python
import os
import re
import sys
import csv
import time
import serial
import datetime
from subprocess import Popen, PIPE, STDOUT


def read_temp(probe_number):
    print("Sending command: $" + str(probe_number) + 'RD\\r\\n')
    ser.write("$" + str(probe_number) + 'RD\r\n')
    out = ''
    # let's wait one second before reading output (let's give device time to answer)
    print("Waiting for 1 seconds before reading")
    time.sleep(1)
    while ser.inWaiting() > 0:
        out += ser.read(1)
    if out != '':
        print("Received: " + out)
        return out
    else:
        print("ERROR: Did not receive")
        return ''


def getSerialPort():
  command = "/usr/local/sbin/xuartctl --port 0 --server --speed 300"
  p = Popen(command, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True)
  output = p.stdout.read()
  regex = re.compile("ttyname=(.*)",re.DOTALL)
  r = regex.search(output)
  return r.groups()[0].strip()


print "Getting temperatures..."
print "Getting serial port to use..."
port = getSerialPort()
print "Going to use port: " + port
print "Setting up serial port..."
# configure the serial connections
ser = serial.Serial(
    port=port,
    baudrate=300,
    parity=serial.PARITY_NONE,
    stopbits=serial.STOPBITS_ONE,
    bytesize=serial.EIGHTBITS
)
already_open = ser.isOpen()
if not already_open:
    ser.open()


print "Getting datetime stamp..."
now = datetime.datetime.now()
date = now.strftime("%m/%d/%Y")
timestamp = now.strftime("%I:%M:%S %p")
print "Using datetime stamp of: " + timestamp


csv_row = []
csv_row.append(date)
csv_row.append(timestamp)
for i in range(1, 5):
    #Will get: *+00024.00
    data = read_temp(i)
    print "Read temp as: " + data
    if data[2] == "-":
        temp_c = data[2:]
    else:
        temp_c = data[3:]
    temp_f = str(float(temp_c) * 9.0 / 5.0 + 32)
    csv_row.append(temp_f)


print "Going to write the following to /mnt/usb/cabin_temps.csv: "
print csv_row


ofile_usb = open("/mnt/usb/cabin_temps.csv", 'a')
writer = csv.writer(ofile_usb, quoting=csv.QUOTE_NONNUMERIC)
writer.writerow(csv_row)
ofile_usb.close()


print "Done collecting temperatures"

好的,太棒了。有了编写和工作的脚本,您可以轻松地将其安排为每小时作为 cronjob 运行。在将系统配置为从只读文件系统启动之前(如果您选择这样做),请确保您已正确设置 SSH 并且您正在使用静态 IP 地址。这将是您从 Web 仪表板服务器收集数据所必需的。否则,这将处理此服务器(我们称之为温度服务器)。如果需要,您可以放心地离开并锁上壁橱门。TS-7500 的额定工作温度范围为 0°C (32°F) 到 70°C (158°F),但已测试可承受低至 -40°C (-40°F) 的温度。这个特定的项目自 2011 年以来一直在安装和运行,并成功经受住了蒙大拿州严酷的冬季,记录到的最低室外温度达到 -40°F(记录到的最高温度为 95°F,供那些好奇的人参考)。

项目难题的下一部分是设置一个 Web 服务器,该服务器将从温度服务器收集数据、存储数据并在美观的仪表板中显示数据。

设置仪表板

对于此步骤,您需要设置一个 Web 服务器。为了我们的缘故,考虑到偏远地区不可靠的电源和连接(更不用说像拨号一样的互联网速度),我们选择在另一个更可靠的位置设置服务器。从技术上讲,您可以直接从 TS-7500 运行 Web 服务器,并拥有一个一体化、低功耗、更简单的解决方案。您需要决定走哪条路,但对于这个项目,我们使用了单独的服务器。只要服务器具有写入数据存储/数据库和运行计划作业的能力,您就可以使用任何您喜欢的,无论是物理专用服务器、虚拟机、共享主机还是云解决方案。

一般步骤是定期(计划 cronjob)使用脚本和 SSH 从温度服务器下载数据,将该数据存储到 MySQL 数据库中,使用 PHP 脚本读取该数据,解析并将其处理成 JSON 格式,供 Google Charts API 理解,最后在网页上显示它。

诚然,这一步有很多内容。作为提醒,为了保持动力,我们做这一切的原因是为了能够根据历史数据更好地为即将到来的寒冷月份做好准备。对我们来说,它回答了“我们什么时候需要开始担心过冬?”这个问题。对您来说,它可能是不同的东西,在这种情况下,您可以根据需要进行调整。下图显示了我们努力实现的仪表板。

数据库

我们首先需要的是一个数据库来存储数据。选择 MySQL 是因为它是一个熟悉的工具。创建了数据库“cabinstats”和表“cabintemps”来保存温度数据


mysql> use cabinstats;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A


Database changed


mysql> show tables;
+----------------------+
| Tables_in_cabinstats |
+----------------------+
| cabintemps           |
+----------------------+
1 rows in set (0.00 sec)


mysql> desc cabintemps;
+-----------+--------------+------+-----+---------+----------------+
| Field     | Type         | Null | Key | Default | Extra          |
+-----------+--------------+------+-----+---------+----------------+
| id        | int(15)      | NO   | PRI | NULL    | auto_increment |
| timestamp | datetime     | YES  |     | NULL    |                |
| box       | decimal(4,1) | YES  |     | NULL    |                |
| lbath     | decimal(4,1) | YES  |     | NULL    |                |
| t_out     | decimal(4,1) | YES  |     | NULL    |                |
| t_in      | decimal(4,1) | YES  |     | NULL    |                |
+-----------+--------------+------+-----+---------+----------------+
6 rows in set (0.01 sec)

计划下载脚本

接下来我们需要的是一个脚本,定期从温度服务器获取温度。同样,Python 相当快地完成了这项任务。它被设置为每小时左右运行一次。这是我们的脚本最终的样子:


get-cabin-temps.py


#!/usr/bin/python


import urllib, urllib2, csv, os, time
from datetime import datetime
from time import gmtime, strftime
#import subprocess as sp
from subprocess import Popen, PIPE
import MySQLdb


class TempDB(object):
    ''' '''


    def __init__(self, _host, _un, _pw, _db, _table):
      '''Initially establish connection with DB.'''


      # Define the name of the table that will keep the stats
      self.table = _table


      try:
        self.conn = MySQLdb.connect(host = _host,
                                    user = _un,
                                    passwd = _pw,
                                    db = _db)
        self.cursor = self.conn.cursor(MySQLdb.cursors.DictCursor)
      except MySQLdb.Error, e:
        raise Exception("Error %d: %s" % (e.args[0], e.args[1]))


    def add_temp_to_db(self, ts, t1, t2, t3, t4):
      ''' '''


      print "Adding to DB (%s)" % ts
      cursor = self.cursor


      cursor.execute("""
          SELECT id FROM %s WHERE timestamp='%s' AND box='%s' AND t_out='%s'
        """ % (self.table, ts, t1, t3))
      if cursor.rowcount == 0:
        cursor.execute("""
            INSERT INTO %s (timestamp,box,lbath,t_out,t_in) VALUES ('%s','%s','%s','%s','%s')
          """ % (self.table, ts, t1, t2, t3, t4))
        return True
      else:
        return False




class TempCsv(object):
  ''' '''


  def __init__(self, host='', file='', dest=''):
    ''' '''


    self.host = host
    self.file = file
    self.dest = dest


  def parse_csv_file(self):
    ''' '''


    #[(1,2,3,4),(1,2,3,4)]
    temps = []
    temp_reader = csv.reader(open(self.dest, 'rU'))
    for row in temp_reader:
      colnum = 0
      for col in row:
        if colnum == 0:
          date = col
        elif colnum == 1:
          the_time = col
        elif colnum == 2:
          box = col
        elif colnum == 3:
          lbath = col
        elif colnum == 4:
          outside = col
        elif colnum == 5:
          inside = col
        colnum += 1


      #Convert to datetime for MySQL
      #0  : 12/21/2011
      #1  : 8:01:50 PM  OR 8:01 PM
      tmp = date + " " + the_time
      try:
        date_obj = time.strptime(tmp, '%m/%d/%Y %I:%M:%S %p')
      except ValueError:
        date_obj = time.strptime(tmp, '%m/%d/%Y %I:%M %p')


      ts = time.strftime('%Y-%m-%d %H:%M:%S', date_obj)


      print "Appended to temps %s: %s %s %s %s\n" % (ts,box,lbath,outside,inside)
      temps.append((ts,box,lbath,outside,inside))


    return temps

  def get_temps(self):
    ''' '''


    os.system('scp "%s:%s" "%s"' % (self.host, self.file, self.dest))
    print "Finished downloading the file.  Getting ready to parse it.\n"
    return self.parse_csv_file()




def main():
  ''' '''


  host = "root@example.dyndns.org"
  file = "/mnt/usb/cabin_temps.csv"
  dest = "~/cabin_temps_master.csv"


  db_host = 'localhost'
  db_name = 'cabinstats'
  db_un = 'root'
  db_pw = 'password'
  db_table = 'cabintemps'


  tempdb = TempDB(db_host, db_un, db_pw, db_name, db_table)
  tempcsv = TempCsv(host, file, dest)

  print "Downloading and importing csv file to DB..."
  temps_table = tempcsv.get_temps()


  print "Getting ready to insert all temps...\n"
  for ts, t1, t2, t3, t4 in temps_table:
      if tempdb.add_temp_to_db(ts, t1, t2, t3, t4):
        print "Adding to temps array: %s, %s, %s, %s, %s" % (ts, t1, t2, t3, t4)


if __name__ == '__main__':
   main()

仪表板代码

让我们看一下仪表板背后的代码。这是一个非常典型的 HTML/JavaScript/PHP 设置,其中 JavaScript 调用 PHP 函数来获取处理后的数据,以便与 Google 的 Chart API 一起使用,然后在 HTML 中的相应 DIV 中绘制图表。这里有很大的改进空间,但它可以工作。以下是负责显示数据的主要代码片段,分为 HTML、JavaScript 和 PHP 部分。

HTML

这就是此页面的全部内容——只有几个元素,其中 <div> 元素最重要。这些元素都有自己的 ID,JavaScript 使用这些 ID 来知道要更新哪个 div 内容


<html>
  <head>
    <title>The Cabin Temperature Dashboard</title>
  </head>

  <body>
    <h1>Current Temperatures (°F):</h1>
    <h2>Last updated: <?=get_currenttempsdate($db);?></h2>
    <div id='currenttemps'></div>

    <h1>Temperature Timeline:</h1>
    <h2>A timeline that displays all temperatures recorded.</h2>
    <div id="timelinetemps" style='width: 1000px; height: 400px;'></div>
  </body>
</html>

Javascript

深入了解我们网站代码的下一层,我们看到了负责加载 Google Charts API 并使用它的 JavaScript。有一些内联 PHP 调用会将 JSON 数据直接插入到脚本中,使事情相当简洁


<script type='text/javascript' src='https://www.google.com/jsapi'></script>
<script type='text/javascript'>
  google.load('visualization', '1', {packages: ['corechart', 'table', 'gauge', 'annotatedtimeline']});


  google.setOnLoadCallback(draw_currenttemps);
  google.setOnLoadCallback(draw_timelinetemps);


  function draw_currenttemps() {
     var data = new google.visualization.DataTable();
     data.addColumn('string', 'Label');
     data.addColumn('number', 'Value');
     <?=get_currenttemps($db);?>


     var opts = {
        width: 650, height: 300,
        greenColor: '#6A93D4',
        greenFrom: -20, greenTo: 31,
        redFrom: 90, redTo: 120,
        yellowFrom:60, yellowTo: 90,
        minorTicks: 10, min: -20, max: 110
     };

     var table = new google.visualization.Gauge(document.getElementById('currenttemps'));
     table.draw(data, opts);
  }


  function draw_timelinetemps() {
     var data = new google.visualization.DataTable();


     data.addColumn('datetime', 'Date');
     data.addColumn('number', 'Outside');
     data.addColumn('string', 'title1');
     data.addColumn('string', 'text1');
     data.addColumn('number', 'Pit');
     data.addColumn('number', 'Inside');
     data.addColumn('number', 'Bath');
     <?=get_timelinetemps($db);?>


     var opts = {
         displayRangeSelector: true,
         displayZoomButtons: true,
         thickness: 2,
         scaleType: 'maximized',                                                            
         displayAnnotations: true,
     };


     var table = new google.visualization.AnnotatedTimeLine(document.getElementById('timelinetemps'));
     table.draw(data, opts);
  }
</script>

PHP

最后,这是从数据库中读取数据并将其处理成 Google Charts 可以理解的内容的 PHP 代码。请注意,我没有提供所有代码,原因有几个:1) 代码是在我意识到使用已弃用的 mysql_query() PHP 调用而不是 PDO 之前编写的,2) 它会让人不知所措,3) 它主要只是辅助函数。只需注意我正在传入一个 $db 对象,该对象是使用 $db = new DB(); 在 PHP 脚本中早期实例化的。除此之外,这段代码就没有太多其他内容了


function get_currenttemps($db) {                                                            
   $query_ct = "SELECT timestamp, box, lbath, t_out, t_in FROM cabintemps order by timestamp desc limit 1;";
   $db->run_query($query_ct);
   $temp_time = "";
   $temps_value = "";
   while ($line = mysql_fetch_array($db->get_result(), MYSQL_ASSOC)) {
       $temp_datetime = strtotime($line['timestamp']);
       $temp_time = date("m/d/y g:i A", $temp_datetime);
       $temps_value = "data.addRows([['Outside', %s], ['Pit', %s], ['Inside', %s], ['Bath', %s]]);";
       $temps = sprintf($temps_value, $line['t_out'], $line['t_in'], $line['box'], $line['lbath']);
   }


   return $temps;
}


function get_timelinetemps($db) {
   $interval = "6 MONTH";
   $query_ct = "SELECT timestamp, box, lbath, t_out, t_in FROM cabintemps WHERE timestamp > DATE_SUB(CURDATE(), INTERVAL $interval) order by timestamp;";
   $db->run_query($query_ct);
   $all_temps_array = array();
   $all_temps_value = "";
   while ($line = mysql_fetch_array($db->get_result(), MYSQL_ASSOC)) {
       $temp_datetime = strtotime($line['timestamp']);
       $temp_all_time = date("Y, m-1, d, H, i", $temp_datetime);



       # $all_temps_value = "[new Date ( year, month-1, day, hour, minute ), outside, inside, server, bath],";
       $all_temps_value = "[new Date ( $temp_all_time ), " . $line['t_out'] . ", undefined, undefined, " . $line['t_in'] . ", " . $line['box'] . ", " . $line['lbath'] .
"],";
       array_push($all_temps_array, $all_temps_value);
   }


   #find min and max
   $query_max = "SELECT * from cabintemps WHERE t_out = (SELECT MAX(t_out) FROM cabintemps WHERE timestamp > DATE_SUB(CURDATE(), INTERVAL $interval)) order by timestamp
desc limit 1";
   $db->run_query($query_max);
   while ($line = mysql_fetch_array($db->get_result(), MYSQL_ASSOC)) {
       $temp_datetime = strtotime($line['timestamp']);
       $temp_all_time = date("Y, m-1, d, H, i", $temp_datetime);


       #$all_temps_value = "[new Date ( $temp_all_time ), " . $line['t_out'] . ", " . $line['t_in'] . ", " . $line['box'] . ", " . $line['lbath'] . ", 'High Outside Temp
: ', '" . $line['t_out'] . "\u00B0F'],";
       $all_temps_value = "[new Date ( $temp_all_time ), undefined, 'High Outside Temp: ', '" . $line['t_out'] . "\u00B0F', undefined, undefined, undefined],";
       array_push($all_temps_array, $all_temps_value);
   }


   $query_min = "SELECT * from cabintemps WHERE t_out = (SELECT MIN(t_out) FROM cabintemps WHERE timestamp > DATE_SUB(CURDATE(), INTERVAL $interval)) order by timestamp
desc limit 1";
   $db->run_query($query_min);
   while ($line = mysql_fetch_array($db->get_result(), MYSQL_ASSOC)) {
       $temp_datetime = strtotime($line['timestamp']);
       $temp_all_time = date("Y, m-1, d, H, i", $temp_datetime);


       #$all_temps_value = "[new Date ( $temp_all_time ), " . $line['t_out'] . ", " . $line['t_in'] . ", " . $line['box'] . ", " . $line['lbath'] . ", 'Low Outside Temp:
 ', '" . $line['t_out'] . "\u00B0F'],";
       $all_temps_value = "[new Date ( $temp_all_time ), undefined, 'Low Outside Temp: ', '" . $line['t_out'] . "\u00B0F', undefined, undefined, undefined],";
       array_push($all_temps_array, $all_temps_value);
   }


   $start_string = "data.addRows([\n";
   $data_row_string_all_temps = implode("\n", $all_temps_array);
   $end_string = "\n]);";


   $all_temps_data = $start_string . $data_row_string_all_temps . $end_string;


   return $all_temps_data;
}

希望这能帮助您基本了解构成 Web 仪表板的不同代码部分。同样,我们有一个脚本来下载原始数据并将其存储到数据库中,我们有一个网页将从数据库中读取数据并以人类友好的方式显示它。这是完成我们项目的最后一块拼图。

结论

这个项目是一次有趣且有益的学习经历。我们能够提出一个强大的解决方案,使规划和监控房屋变得轻松而有意义。能够从世界任何地方查看您家(或在本例中是小木屋)的当前状况非常方便,并且有助于规划,这样您的管道就不会爆裂。在高温是一个问题的情况下,例如对于狗舍或谷仓中的宠物或牲畜,它也很方便。当然,实现这一目标的方法不止一种,有些比其他方法更简单。例如,如果我要再次这样做,我会使用更多的 AJAX 调用进行实时温度轮询,而不是计划轮询。我鼓励您根据您从本项目中学到的知识,提出您自己的解决方案,以满足您的需求并适合您的特定情况,然后在基础上进行构建。这在个人和工业商业应用中都具有潜力,例如家庭自动化,所以请受到启发并开始构建!

如果您对项目有任何问题或意见,或者您有改进项目的想法,请务必使用下面的评论表。

加载 Disqus 评论