温湿度・気圧センサーの値をThingSpeakでグラフ化し、スマホアプリで確認する方法

温湿度・気圧センサーの値をThingSpeakでグラフ化し、スマホアプリで確認する方法
Page content

本記事は次の記事の続編です。

第3弾の今回は、自宅サーバRaspberry Pi 4に接続したセンサーBME280の温度・気圧・湿度の時系列データを、ThingSpeakでグラフ化することが目標です。

温度・気圧・湿度をグラフ化する手順

Step-1. ThingSpeak側の準備

今回のグラフ化では、ThingSpeakの「FREE License」を利用します。ThingSpeakとはなにかというと、運営元MathWorksのサイトにはこう書かれています。

ThingSpeak は Internet of Things (IoT) プラットフォームであり、クラウドでセンサーのデータを収集および保存したり、IoT アプリケーションを開発したりできます。ThingSpeak™ IoT プラットフォームでは MATLAB® でデータを解析し可視化できるアプリを提供しており、そのデータに基づいてアクションを実行することができます。センサー データは Arduino®、Raspberry Pi™、BeagleBone Black、および他のハードウェアから ThingSpeak に送信できます。

私はこれまで、ThingSpeakを知りませんでしたが、IoTのデータを蓄積・分析するプラットフォームとして、世界中の人々に広く使われているような雰囲気があります。また、サードパーティのアプリ (後述) を用いると、スマホでもグラフがすばやく閲覧できて素敵です。

ThingSpeak側で行う準備は次の通りです。

  1. アカウントの作成: ThingSpeakの「Get Started For Free」からアカウントを作成します。メールアドレスの登録が必要です
  2. Channelの作成と設定: My Channelsでチャンネルをひとつ作成し、SettingsでField 1,2,3を有効化し、この順番で「温度 (℃)」「気圧 (hPa)」「湿度 (%)」と名前を付けます
  3. データ書き込み用のAPIキーの取得: 作成したチャンネルにデータを書き込むときに必要な、16文字の「Write API Key」をメモしておきます

Step-2. Pythonスクリプトの準備

今回の目標達成のために、スイッチサイエンス社から提供されているサンプルコード bme280_sample.py を改造して、bme280_thingspeak.py を作成しました (ソースコードは最後に掲載)。改造のポイントは次の通りです。

  1. Python 3対応
  2. 各値のoversamplingを「x1」から「x8」に変更
  3. ThingSpeak.comへのデータ送信機能を追加
    • 第1引数を付与して実行すると、その引数をThingSpeakの「Write API Key」とみなして、測定値3つをThingSpeakへ送信します。第1引数が付与されていないときの挙動は bme280_sample.py と同じです

bme280_thingspeak.py は次のような形で実行します。「Write API Key」を、ここでは仮に 0123456789ABCDEF としています。

$ python3 bme280_thingspeak.py "0123456789ABCDEF"
temp : 18.00  ℃
pressure : 1025.58 hPa
hum :  51.59 %
ThingSpeak : 200

一度手動でテスト実行して問題がなければ、次に、Raspberry Piサーバ上のcronなどで定期実行を仕掛けます。

追記 [2021-01-05]

bme280_thingspeak.py の定期実行を続けて気づいたこと。サーバ再起動後、センサーから最初に取得するデータは、たとえば気圧なら600hPaぐらいと、異常な値になる傾向に気づきました。そこでcron 1回実行につき、次のようにスクリプトを2回実行し、1回目のデータは捨てて、2回目のデータをThingSpeakへ送信するようにしました。毎回のcron実行時にこれをやるのは無駄ですが、暫定対応として。

# [room-condition]
*/2 * * * * timeout 20s /home/foobar/bme280_thingspeak.py >/dev/null; sleep 10; timeout 20s /home/foobar/bme280_thingspeak.py "0123456789ABCDEF" >/dev/null 2>&1

Step-3. ThingSpeakサイトで確認

実際にcronで2分ごとにデータ送信を行っているデータを、ThingSpeakサイトで確認すると次のようになります。

Step-4. スマホで確認

アプリで確認

Step-3のデータを、Androidアプリの ThingView - ThingSpeak viewer (無料版) で閲覧した画面は次のようになります。ちなみに、閲覧する時間幅を調整する機能は無料版にはなく、このアプリの有料版 ThingView Full に備わっています。

ホーム画面のWidgetで確認

Step-3のデータを、Androidアプリの IoT ThingSpeak Monitor Widget の、ホーム画面上のWidgetとして表示した画面は次のようになります。Widget右下のグラフのアイコンを押すと、全画面でグラフも描画されます。

感想

「部屋の温度・気圧・湿度の変化をグラフで見たい……」という思いつきから始めた、Raspberry Piとセンサーを接続したIoT的な試みは、私の初めてにしては意外なほど、いい感じの完成度となりました (自己満足)。ThingSpeakとその関連アプリが整備されているおかげで、部屋内のセンサーの各測定値やグラフをスマホの画面まで自動的に届けることが、こんなに簡単にできるとは。

この次は、センサーを別途手に入れ次第、二酸化炭素濃度のグラフ化をやってみるつもりです。

追記 [2021-01-25]

ソースコード

bme280_thingspeak.py

今回作成した bme280_thingspeak.py のソースコードは次の通りです。オリジナルとのdiffも掲載します。

#!/usr/bin/env python3

# Original: https://github.com/SWITCHSCIENCE/samplecodes/blob/master/BME280/Python27/bme280_sample.py

from smbus2 import SMBus
import time
import sys
import requests

bus_number  = 1
i2c_address = 0x76

bus = SMBus(bus_number)

digT = []
digP = []
digH = []

t_fine = 0.0

if len(sys.argv) > 1:
	thingspeak_api_key = sys.argv[1] # ThingSpeak's Write API Key
else:
	thingspeak_api_key = ''

def post_thingspeak(value1, value2, value3):
	url = 'https://api.thingspeak.com/update'
	params = {
		'api_key': thingspeak_api_key,
		'field1': value1, # temp
		'field2': value2, # pressure
		'field3': value3, # hum
	}
	r = requests.get(url, params=params)
	return r.status_code

def writeReg(reg_address, data):
	bus.write_byte_data(i2c_address,reg_address,data)

def get_calib_param():
	calib = []
	
	for i in range (0x88,0x88+24):
		calib.append(bus.read_byte_data(i2c_address,i))
	calib.append(bus.read_byte_data(i2c_address,0xA1))
	for i in range (0xE1,0xE1+7):
		calib.append(bus.read_byte_data(i2c_address,i))

	digT.append((calib[1] << 8) | calib[0])
	digT.append((calib[3] << 8) | calib[2])
	digT.append((calib[5] << 8) | calib[4])
	digP.append((calib[7] << 8) | calib[6])
	digP.append((calib[9] << 8) | calib[8])
	digP.append((calib[11]<< 8) | calib[10])
	digP.append((calib[13]<< 8) | calib[12])
	digP.append((calib[15]<< 8) | calib[14])
	digP.append((calib[17]<< 8) | calib[16])
	digP.append((calib[19]<< 8) | calib[18])
	digP.append((calib[21]<< 8) | calib[20])
	digP.append((calib[23]<< 8) | calib[22])
	digH.append( calib[24] )
	digH.append((calib[26]<< 8) | calib[25])
	digH.append( calib[27] )
	digH.append((calib[28]<< 4) | (0x0F & calib[29]))
	digH.append((calib[30]<< 4) | ((calib[29] >> 4) & 0x0F))
	digH.append( calib[31] )
	
	for i in range(1,2):
		if digT[i] & 0x8000:
			digT[i] = (-digT[i] ^ 0xFFFF) + 1

	for i in range(1,8):
		if digP[i] & 0x8000:
			digP[i] = (-digP[i] ^ 0xFFFF) + 1

	for i in range(0,6):
		if digH[i] & 0x8000:
			digH[i] = (-digH[i] ^ 0xFFFF) + 1  

def readData():
	data = []
	for i in range (0xF7, 0xF7+8):
		data.append(bus.read_byte_data(i2c_address,i))
	pres_raw = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
	temp_raw = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4)
	hum_raw  = (data[6] << 8)  |  data[7]
	
	compensate_T(temp_raw)
	compensate_P(pres_raw)
	compensate_H(hum_raw)

def compensate_P(adc_P):
	global  t_fine
	pressure = 0.0
	
	v1 = (t_fine / 2.0) - 64000.0
	v2 = (((v1 / 4.0) * (v1 / 4.0)) / 2048) * digP[5]
	v2 = v2 + ((v1 * digP[4]) * 2.0)
	v2 = (v2 / 4.0) + (digP[3] * 65536.0)
	v1 = (((digP[2] * (((v1 / 4.0) * (v1 / 4.0)) / 8192)) / 8)  + ((digP[1] * v1) / 2.0)) / 262144
	v1 = ((32768 + v1) * digP[0]) / 32768
	
	if v1 == 0:
		return 0
	pressure = ((1048576 - adc_P) - (v2 / 4096)) * 3125
	if pressure < 0x80000000:
		pressure = (pressure * 2.0) / v1
	else:
		pressure = (pressure / v1) * 2
	v1 = (digP[8] * (((pressure / 8.0) * (pressure / 8.0)) / 8192.0)) / 4096
	v2 = ((pressure / 4.0) * digP[7]) / 8192.0
	pressure = pressure + ((v1 + v2 + digP[6]) / 16.0)  

	print("pressure : %7.2f hPa" % (pressure/100))
	global post_pressure
	post_pressure = pressure/100

def compensate_T(adc_T):
	global t_fine
	v1 = (adc_T / 16384.0 - digT[0] / 1024.0) * digT[1]
	v2 = (adc_T / 131072.0 - digT[0] / 8192.0) * (adc_T / 131072.0 - digT[0] / 8192.0) * digT[2]
	t_fine = v1 + v2
	temperature = t_fine / 5120.0
	print("temp : %-6.2f ℃" % (temperature))
	global post_temperature
	post_temperature = temperature

def compensate_H(adc_H):
	global t_fine
	var_h = t_fine - 76800.0
	if var_h != 0:
		var_h = (adc_H - (digH[3] * 64.0 + digH[4]/16384.0 * var_h)) * (digH[1] / 65536.0 * (1.0 + digH[5] / 67108864.0 * var_h * (1.0 + digH[2] / 67108864.0 * var_h)))
	else:
		return 0
	var_h = var_h * (1.0 - digH[0] * var_h / 524288.0)
	if var_h > 100.0:
		var_h = 100.0
	elif var_h < 0.0:
		var_h = 0.0
	print("hum : %6.2f %" % (var_h))
	global post_humidity
	post_humidity = var_h

def setup():
	#osrs_t = 1			#Temperature oversampling x 1
	osrs_t = 4			#Temperature oversampling x 8
	#osrs_p = 1			#Pressure oversampling x 1
	osrs_p = 4			#Pressure oversampling x 8
	#osrs_h = 1			#Humidity oversampling x 1
	osrs_h = 4			#Humidity oversampling x 8
	mode   = 3			#Normal mode
	t_sb   = 5			#Tstandby 1000ms
	filter = 0			#Filter off
	spi3w_en = 0			#3-wire SPI Disable

	ctrl_meas_reg = (osrs_t << 5) | (osrs_p << 2) | mode
	config_reg    = (t_sb << 5) | (filter << 2) | spi3w_en
	ctrl_hum_reg  = osrs_h

	writeReg(0xF2,ctrl_hum_reg)
	writeReg(0xF4,ctrl_meas_reg)
	writeReg(0xF5,config_reg)


setup()
get_calib_param()


if __name__ == '__main__':
	try:
		readData()
		if len(thingspeak_api_key) > 0:
			p = post_thingspeak(
				'{:.02f}'.format(post_temperature),
				'{:.02f}'.format(post_pressure),
				'{:.02f}'.format(post_humidity))
			print("ThingSpeak : %d" % (p))
	except KeyboardInterrupt:
		pass

オリジナルとのdiff

$ diff -up ./bme280_sample.py.20160829 ./bme280_thingspeak.py
--- ./bme280_sample.py.20160829	2020-12-20 17:04:09.554538956 +0900
+++ ./bme280_thingspeak.py	2020-12-20 18:30:36.845542555 +0900
@@ -1,7 +1,11 @@
-#coding: utf-8
+#!/usr/bin/env python3
+
+# Original: https://github.com/SWITCHSCIENCE/BME280/blob/master/Python27/bme280_sample.py
 
 from smbus2 import SMBus
 import time
+import sys
+import requests
 
 bus_number  = 1
 i2c_address = 0x76
@@ -14,6 +18,21 @@ digH = []
 
 t_fine = 0.0
 
+if len(sys.argv) > 1:
+	thingspeak_api_key = sys.argv[1] # ThingSpeak's Write API Key
+else:
+	thingspeak_api_key = ''
+
+def post_thingspeak(value1, value2, value3):
+	url = 'https://api.thingspeak.com/update'
+	params = {
+		'api_key': thingspeak_api_key,
+		'field1': value1, # temp
+		'field2': value2, # pressure
+		'field3': value3, # hum
+	}
+	r = requests.get(url, params=params)
+	return r.status_code
 
 def writeReg(reg_address, data):
 	bus.write_byte_data(i2c_address,reg_address,data)
@@ -92,7 +111,9 @@ def compensate_P(adc_P):
 	v2 = ((pressure / 4.0) * digP[7]) / 8192.0
 	pressure = pressure + ((v1 + v2 + digP[6]) / 16.0)  
 
-	print "pressure : %7.2f hPa" % (pressure/100)
+	print("pressure : %7.2f hPa" % (pressure/100))
+	global post_pressure
+	post_pressure = pressure/100
 
 def compensate_T(adc_T):
 	global t_fine
@@ -100,7 +121,9 @@ def compensate_T(adc_T):
 	v2 = (adc_T / 131072.0 - digT[0] / 8192.0) * (adc_T / 131072.0 - digT[0] / 8192.0) * digT[2]
 	t_fine = v1 + v2
 	temperature = t_fine / 5120.0
-	print "temp : %-6.2f ℃" % (temperature) 
+	print("temp : %-6.2f ℃" % (temperature))
+	global post_temperature
+	post_temperature = temperature
 
 def compensate_H(adc_H):
 	global t_fine
@@ -114,13 +137,17 @@ def compensate_H(adc_H):
 		var_h = 100.0
 	elif var_h < 0.0:
 		var_h = 0.0
-	print "hum : %6.2f %" % (var_h)
-
+	print("hum : %6.2f %" % (var_h))
+	global post_humidity
+	post_humidity = var_h
 
 def setup():
-	osrs_t = 1			#Temperature oversampling x 1
-	osrs_p = 1			#Pressure oversampling x 1
-	osrs_h = 1			#Humidity oversampling x 1
+	#osrs_t = 1			#Temperature oversampling x 1
+	osrs_t = 4			#Temperature oversampling x 8
+	#osrs_p = 1			#Pressure oversampling x 1
+	osrs_p = 4			#Pressure oversampling x 8
+	#osrs_h = 1			#Humidity oversampling x 1
+	osrs_h = 4			#Humidity oversampling x 8
 	mode   = 3			#Normal mode
 	t_sb   = 5			#Tstandby 1000ms
 	filter = 0			#Filter off
@@ -142,6 +169,12 @@ get_calib_param()
 if __name__ == '__main__':
 	try:
 		readData()
+		if len(thingspeak_api_key) > 0:
+			p = post_thingspeak(
+				'{:.02f}'.format(post_temperature),
+				'{:.02f}'.format(post_pressure),
+				'{:.02f}'.format(post_humidity))
+			print("ThingSpeak : %d" % (p))
 	except KeyboardInterrupt:
 		pass