HTML
JavaScript
IoT
WebBluetooth
BlueJelly

お天気APIの気温が本当に正しいのか、IoTガジェットを作って確かめてみた

概要

温度センサとお天気APIの両方からそれぞれ温度を取得し、温度データをクラウドへ保存するIoTを作りました。ラズパイ(BLE/WiFi Gateway)にディスプレイを繋げれば直近のデータが見られるし、サーバーへ保存したまとまったデータはPCやスマホからグラフで確認することも可能です。

Qiitaの図180110.png

IoT風に言い換えれば、エッジコンピューティングで高頻度のリアルタイム短期データを確認して、クラウドコンピューティングで低頻度の長期データを検証します。木も見て森も見る。何だかカッコいいっすね。

ちなみに、AWSやAzure、Herokuなどのサーバーを準備したり、サーバーサイドのプログラミングを行っていません。サーバー側は全てサービスを利用して、ローカル側からAPIで通信しているのみです。BLE Device以外は、HTML/JavaScript/CSSでプログラミングしています。実行場所も全てローカルです。

BLE Device

温度センサ

温度センサ.jpg

温度センサは秋月電子通商で500円くらいで売っているアナログ・デバイセズ社のADT7410を使用しました。
通信はI2C(アイ・スクエア・シー)という規格で行いますが、プルアップという処理が必要になります。
このセンサーはモジュールになっていて、指定の箇所を半田付けするだけでプルアップできます。

I2Cに関しては以下のページの第5回目の記事、半田付け方法についてはコラム2を見ると良く分かります
ハードウェアのプロが教えるWebエンジニアのためのIoT講座

BLEマイコン

image.png

マイコンはRedBear社のBLE Nano v2を使用しました。
書き込み器とマイコン部が切り離せるため、非常に小さくできるのが特徴です。
今回のようなシステムではWiFi付マイコンでも出来ますが、BLEマイコンを使うことにより低消費電力で小型化できるのが嬉しいポイントです。

その他のハードウェア

image.png
温度センサーとBLEマイコン以外に以下のモノを使っています。
電池はコイン電池でもいけますが、安定した電圧供給と長期間放置に耐えられるように、モバイルバッテリーを使用しました。

  • ブレッドボード
  • ジャンパーワイヤ
  • microUSBコネクタ
  • microUSBケーブル
  • モバイルバッテリー

※常時、クラウドと情報をやり取りするために、インターネットに接続している必要があるため、WiFiルーターも必要です(普段、家の中で使っているWiFiルーターを使用しました)

image.png
結線は写真の通りです。
上側の赤いジャンパーワイヤが5Vで、下側の赤いジャンパーワイヤが3.3Vです。黒いジャンパーワイヤはGND、黄色はSDA、青がSCLです。

image.png
ポイントは、VINにモバイルバッテリーからの5Vを供給し、VDDから3.3Vを出力させて温度センサーへ電源供給している点です。BLE Nanoは内部に3.3Vのレギュレーターを持っているために、このようなことが可能となっています。

image.png
このハードウェアを家のベランダに数日放置して測定を行いましたが、突然の雨や風などが来るとまずいので、写真のように小さな段ボール箱の中に入れました。

ファームウェア

BLEマイコンのソフトはMbedを使って書き込みました
MbedはARM社のプロトタイピング用マイコンボードのプログラミング環境で、インストール不要でブラウザのみで開発できます。

/*

Copyright (c) 2012-2014 RedBearLab

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.

*/

/*
 *    The application works with the BlueJelly.js
 *
 *    http://jellyware.jp/ 
 *
 */

//======================================================================
//Grobal
//====================================================================== 
//------------------------------------------------------------
//Include Header Files
//------------------------------------------------------------ 
#include "mbed.h"
#include "ble/BLE.h"
#include "ADT7410.h"

//------------------------------------------------------------
//Definition
//------------------------------------------------------------ 
#define TXRX_BUF_LEN 20                     //max 20[byte]
#define DEVICE_LOCAL_NAME "ADT7410 Temperature"  //Change Device name   
#define ADVERTISING_INTERVAL 160            //160 * 0.625[ms] = 100[ms]
#define DIGITAL_OUT_PIN P0_29

//------------------------------------------------------------
//Object generation
//------------------------------------------------------------ 
BLE ble;
DigitalOut      LED_SET(DIGITAL_OUT_PIN);

//I2C Pin setting P0_4=SDA, P0_5=SCL
ADT7410 temp(P0_5, P0_4, 0x90, 10000);


//------------------------------------------------------------
//Service & Characteristic Setting
//------------------------------------------------------------ 
//Service UUID
static const uint8_t base_uuid[] = { 0x71, 0x3D, 0x00, 0x00, 0x50, 0x3E, 0x4C, 0x75, 0xBA, 0x94, 0x31, 0x48, 0xF1, 0x8D, 0x94, 0x1E } ;

//Characteristic UUID
static const uint8_t tx_uuid[]   = { 0x71, 0x3D, 0x00, 0x03, 0x50, 0x3E, 0x4C, 0x75, 0xBA, 0x94, 0x31, 0x48, 0xF1, 0x8D, 0x94, 0x1E } ;
static const uint8_t rx_uuid[]   = { 0x71, 0x3D, 0x00, 0x02, 0x50, 0x3E, 0x4C, 0x75, 0xBA, 0x94, 0x31, 0x48, 0xF1, 0x8D, 0x94, 0x1E } ;

//Characteristic Value
uint8_t txPayload[TXRX_BUF_LEN] = {0,};
uint8_t rxPayload[TXRX_BUF_LEN] = {0,};

//Characteristic Property Setting etc
GattCharacteristic  txCharacteristic (tx_uuid, txPayload, 1, TXRX_BUF_LEN, GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_WRITE | GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_READ);
GattCharacteristic  rxCharacteristic (rx_uuid, rxPayload, 1, TXRX_BUF_LEN, GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_NOTIFY| GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_READ);
GattCharacteristic *myChars[] = {&txCharacteristic, &rxCharacteristic};

//Service Setting
GattService         myService(base_uuid, myChars, sizeof(myChars) / sizeof(GattCharacteristic *));


//======================================================================
//onDisconnection
//======================================================================
void disconnectionCallback(const Gap::DisconnectionCallbackParams_t *params)
{
    ble.startAdvertising();
}


//======================================================================
//convert reverse UUID
//======================================================================
void reverseUUID(const uint8_t* src, uint8_t* dst)
{
    int i;

    for(i=0;i<16;i++)
        dst[i] = src[15 - i];
}


//======================================================================
//main
//======================================================================
int main(void)
{
    uint8_t base_uuid_rev[16];

    //BLE init
    ble.init();

    //EventListener
    ble.onDisconnection(disconnectionCallback);

    //------------------------------------------------------------
    //setup advertising 
    //------------------------------------------------------------
    //Classic BT not support
    ble.accumulateAdvertisingPayload(GapAdvertisingData::BREDR_NOT_SUPPORTED);

    //Connectable to Central
    ble.setAdvertisingType(GapAdvertisingParams::ADV_CONNECTABLE_UNDIRECTED);

    //Local Name
    ble.accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LOCAL_NAME,
                                    (const uint8_t *)DEVICE_LOCAL_NAME, sizeof(DEVICE_LOCAL_NAME) - 1);

    //GAP AdvertisingData                                
    reverseUUID(base_uuid, base_uuid_rev);  
    ble.accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LIST_128BIT_SERVICE_IDS,
                                    (uint8_t *)base_uuid_rev, sizeof(base_uuid));

    //Advertising Interval 
    ble.setAdvertisingInterval(ADVERTISING_INTERVAL);

    //Add Service
    ble.addService(myService);

    //Start Advertising
    ble.startAdvertising(); 

    uint8_t buf[2];
    float tempadt;

    // reset sensor to default values
    temp.reset();

    // reduce sample rate to save power
    temp.setConfig(ONE_SPS_MODE);   

    //------------------------------------------------------------
    //Loop
    //------------------------------------------------------------
    while(1)
    {
        // get temperature every 10 seconds
        tempadt = temp.getTemp();

        int16_t value0 = tempadt;                   //Get integer value
        int16_t value1 = (tempadt - value0)*100;    //Get decimal value
        buf[0] = value0;
        buf[1] = value1;

        //Send out
        ble.updateCharacteristicValue(rxCharacteristic.getValueAttribute().getHandle(), buf, 2);

        ble.waitForEvent(); 

        wait(10);
    }
}

なぜかタイマー割り込みを使うとうまく実行してくれないため、ループとwaitでぐるぐる回しています。
その他、mbed-osやADT7410のライブラリも必要ですので、手順を下に書いておきます

  • 新規作成 > 空のプログラム
  • importで mbed-osをインポート
  • ADT7410をインポート
  • main.cppを新規作成して上記のプログラムを書き込む

BLE/WiFi Gateway

ハードウェア

image.png
BLE/WiFi GatewayのハードウェアとしてRaspberryPi3を使います。
RaspberryPiはイギリスのラズベリーパイ財団が開発しているARMプロセッサを搭載したシングルボードコンピュータです。RaspberryPi3が2018年1月時点では最も高性能でWiFiとBluetoothを標準搭載しています。
ノートPCやスマホ、タブレットでも可能ですが、長時間放置させる必要があるので、ラズパイがぴったりです。

image.png
また、HDMIにディスプレイを繋ぐことで、リアルタイムに高頻度データを確認できます

OS

2017/11/29リリースのRaspian(Stretch)を使っています。
ラズパイでWebBluetoothを使うために1つフラグ設定を行う必要があります

Chromiumにて、以下のアドレスを入力し、"Experimental Web Platform features"を有効にします

chrome://flags

詳しくはこちらを見てください
http://jellyware.jp/kurage/bluejelly/raspi_bluejelly.html

ラズパイはデフォルトでしばらくするとディスプレイ画面がOFFになるように設定されています。マウスやキーボードを動かすとONになります。
いつでも確認できるモニターとして使用するために常時画面ON設定にします。

エディッタでautostartファイルを編集します(以下はNanoエディッタの例)

sudo nano ~/.config/lxsession/LXDE-pi/autostart

最後に以下の4行を追加して上書き保存します

@xset s 0 0
@xset s noblank
@xset s noexpose
@xset dpms 0 0 0

BLE Deviceから気温データ取得

image.png
HTML/JSでBLEと簡単に通信できるBlueJellyを使います。
BlueJellyはWeb Bluetooth APIのラッパーライブラリです。
詳しくはこちら
HTMLとJavaScriptだけでBLE通信できるのか?

お天気APIの温度データ取得

image.png
OpenWeatherMapを使います
メール登録だけで簡単にAPI Keyを入手できます。HTML/JSを使って現在の気温を取得します。
ちなみに、こちらのページを見ると、気象庁からもデータを収集しているようです。
http://openweathermap.org/technology

JSONデータをHTTP(GET)通信で取得

image.png
jQueryを使います
jQueryはJavaScriptで書くと長くなってしまうプログラムを短く書くことができるライブラリです。
JavaScriptだけでも書けますが、ちょっと複雑になるため、jQueryを使って簡潔に記述します。
jQueryを使わない場合はこちらを参考にして下さい。
https://q-az.net/without-jquery-getjson/

リアルタイムグラフ表示

image.png
SmoothieChartsを使います。
JavaScriptで描ける無料のグラフ描画ライブラリです。
有名なD3.jsやchat.jsなどと異なるのは、ストリーミングデータをリアルタイムで表示する点です。
オシロスコープ表示のようなグラフが描けます。
直近の温度値の値が正しく取得できるのかを確認するモニター用にこのグラフを利用します。

クラウドへデータ保存

image.png
Milkcocoaを使います
サーバーへのデータ保存、サーバーからデータ取得が非常にお手軽に行えます。
サーバーサイド側のプログラムはゼロで利用できるBaaS(Backend as a Service)です。
サーバー自体も用意する必要はありません。全て無料の範囲で利用できます。
こちらもJavaScriptで使用可能です。

ちなみに、HTML/JSでデータ保存する方法としてWebストレージ(ローカルストレージ)がありますが、保存した端末でしかデータを見ることしかできません。他の端末でも見られるようにデータ吐き出しする方法もありますが、確認したいときにいちいち吐き出し処理を行うのが面倒なので、クラウドに保存することにしました。

アプリ

image.png
先ほどの5つのライブラリやAPIを使ってHTML/JSで作成しました。お天気APIの温度と実際の温度センサーの温度の差分を表示させています。
ソースコードはこちらです。

<!doctype html>
<!--
Copyright 2017 JellyWare Inc. All Rights Reserved.
-->
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="description" content="Web Bluetooth API Sample">
    <meta name="viewport" content="width=640, maximum-scale=1.0, user-scalable=yes">
    <title>BlueJelly</title>
    <link href="https://fonts.googleapis.com/css?family=Lato:100,300,400,700,900" rel="stylesheet" type="text/css">
    <link rel="stylesheet" href="style.css">
    <script type="text/javascript" src="./bluejelly.js"></script>
    <script type="text/javascript" src="./smoothie.js"></script>
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script src="https://cdn.mlkcca.com/v0.6.0/milkcocoa.js"></script>
</head>
<body>
    <div class="container">
        <div class="title margin">
            <p id="title">お天気API温度とセンサー温度の差分</p>
            <p id="subtitle">Web Bluetooth(BlueJelly)を利用したプログラム</p>
        </div>
        <div class="contents margin">
            <button id="readData" class="button">Read</button>
            <button id="startNotifications" class="button">Start Notify</button>
            <button id="stopNotifications" class="button">Stop Notify</button>
            <button id="reset" class="button">Reset</button>
            <hr>
            <div id="device_name">xxxx</div>
            <img id="icon_all">
            <div id="tenki_all"></div>
            <div id="temp2">xxx</div>
            <div id="temp1">xxx</div>
            <div id="data_text">xxxx</div>

            <!-- データを表示するためのcanvasを追加 -->
            <canvas id="chart" width="500" height="160"></canvas>

        </div>
        <div class="footer margin">
                    For more information, see <a href="http://jellyware.jp/kurage" target="_blank">jellyware.jp</a> and <a href="https://github.com/electricbaka/bluejelly" target="_blank">GitHub</a> !
        </div>
    </div>
    <script>
        //--------------------------------------------------
        //Global変数
        //--------------------------------------------------
        //BlueJellyのインスタンス生成
        var ble = new BlueJelly();
        var ble_data = new TimeSeries(); //Smoothie Chartsの関数を追加

        var value_sensor = "";//未取得状態であることが分かるように初期化
        var value_OpenWeatherMap = "";//未取得状態であることが分かるように初期化

        var TIME_GET_INTERVAL = 10 * 1000;//センサー取得間隔 10秒
        var TIME_SEND_INTERVAL = 300 * 1000;//サーバー送信間隔 5分=300秒
        var counter_interval = 0;//TIME_SEND_INTERVALをカウント

        var milkcocoa = new MilkCocoa('*************************');//Milkcocoaで取得したIDを入力すること
        var ds = milkcocoa.dataStore('data');

        //-------------------------------------------------
        //差分データ取得イベント
        //-------------------------------------------------
        function event_diff(){
          //OpenWeatherMapにて温度情報を更新
          weather();

          if(value_sensor !="")
            document.getElementById('temp1').innerHTML = "Sensor : " + value_sensor + " ℃";
          else
            document.getElementById('temp1').innerHTML = "Sensor : Error";

          if(value_OpenWeatherMap !="")
            document.getElementById('temp2').innerHTML = "API : " + value_OpenWeatherMap + " ℃";
          else
            document.getElementById('temp2').innerHTML = "API : Error";

          //差分を求める
          value_diff = value_OpenWeatherMap - value_sensor;
          console.log("value_OpenWeatherMap : "+ value_OpenWeatherMap);
          console.log("value_sensor : "+ value_sensor);
          console.log("value_diff : "+ value_diff);

          //少数2桁に
          value_diff = value_diff.toFixed(2);

          //グラフに描く
          ble_data.append(new Date().getTime(), value_diff);

          //HTMLに差分値を表示
          document.getElementById('data_text').innerHTML = value_diff + ' ℃';

          //-------------------------------------------------
          //Milkcocoaへ保存
          //-------------------------------------------------
          // Dateオブジェクトを作成
          var date = new Date() ;

          // UNIXタイムスタンプを取得する (ミリ秒単位)
          var unix_time = date.getTime() ;

          counter_interval++;
          if(counter_interval >= (TIME_SEND_INTERVAL/TIME_GET_INTERVAL))
          {
            ds.push({unix_time:unix_time, temp_sensor:value_sensor ,temp_api:value_OpenWeatherMap});
            console.log("<<< Milkcocoaへ保存 >>>");
            counter_interval = 0;
          }

          value_sensor = "";//未取得状態であることが分かるように初期化
          value_OpenWeatherMap = "";//未取得状態であることが分かるように初期化
        }

        //-------------------------------------------------
        //年月日 時:分:秒を取得
        //-------------------------------------------------
        function get_date(){
          var now_date = new Date();

          var year = now_date.getFullYear();
          var month = now_date.getMonth()+1;
          var week = now_date.getDay();
          var day = now_date.getDate();
          var hour = now_date.getHours();
          var minute = now_date.getMinutes();
          var second = now_date.getSeconds();

          var str = year + "/" + month + "/" + day + " " + hour + ":" + minute + ":" + second
          console.log(str);

          return str;
        }


        //-------------------------------------------------
        //OpenWeatherMapから取得
        //-------------------------------------------------
        function weather(){
          $.getJSON(
              "http://api.openweathermap.org/data/2.5/weather",
              {"lat":"35.681167", "lon":"139.767052", "appid":"*******************************"}, //OpenWeatherMapで取得したIDを入力すること
              //東京駅 緯度: 35.681167 経度: 139.767052

              function(data){
                  //天気アイコン表示
                  icon_url = "http://openweathermap.org/img/w/" + data.weather[0].icon + ".png";
                  $('#icon').attr('src', icon_url);

                  //ケルビンから摂氏に換算
                  value = (data.main.temp - 273.15).toFixed(2);

                  //天気情報(詳細)、場所 表示
                  $('#icon_all').attr('src', icon_url);
                  $("#tenki_all").text(data.weather[0].main + "(" + data.weather[0].description + ") " + data.name);

                  value_OpenWeatherMap = value;
              }
          );
        }

        //-------------------------------------------------
        //smoothie.js
        //-------------------------------------------------
        function createTimeline() {
            var chart = new SmoothieChart({
            millisPerPixel: 120, //120 ms x 500pixel = 60000ms
            grid: {
                fillStyle: '#27b34f',
                strokeStyle: '#ffffff',
                millisPerLine: 6000 //60000/6000
                },
                    //maxValue: 10,
                    //minValue: -10
                });
                chart.addTimeSeries(ble_data, {
                    strokeStyle: 'rgba(255, 255, 255, 1)',
                    fillStyle: 'rgba(255, 255, 255, 0.2)',
                    lineWidth: 4
                });
                chart.streamTo(document.getElementById("chart"), 500);
            }

        //-------------------------------------------------
        //ボタンが押された時のイベント登録
        //--------------------------------------------------
        document.getElementById('readData').addEventListener('click', function() {
            ble.read('UUID1');
        });

        document.getElementById('startNotifications').addEventListener('click', function() {
            ble.startNotify('UUID1');
        });

        document.getElementById('stopNotifications').addEventListener('click', function() {
            ble.stopNotify('UUID1');
        });

        document.getElementById('reset').addEventListener('click', function() {
            ble.reset(); //reset is disconnect & clear
        });

        //--------------------------------------------------
        //ロード時の処理
        //--------------------------------------------------
        window.onload = function() {
            //初期の文字列表示
            document.getElementById('device_name').innerHTML = "No Device";
            document.getElementById('data_text').innerHTML = "No Data"

            document.getElementById('temp1').innerHTML = "Sensor : Ready...";
            document.getElementById('temp2').innerHTML = "API :  Ready...";

            //UUIDの設定
            ble.setUUID("UUID1", "713d0000-503e-4c75-ba94-3148f18d941e", "713d0002-503e-4c75-ba94-3148f18d941e"); //BLEnano SimpleControl
            ble.setUUID("UUID2", "713d0000-503e-4c75-ba94-3148f18d941e", "713d0003-503e-4c75-ba94-3148f18d941e"); //BLEnano SimpleControl

            //smoothie.jsを追加
            createTimeline();

            //天気情報を最初に取得
            weather();

            //差分イベントのタイマー発動
            setInterval(event_diff, TIME_GET_INTERVAL);
        }


        //--------------------------------------------------
        //Scan後の処理
        //--------------------------------------------------
        ble.onScan = function(deviceName) {
            //HTMLに表示
            document.getElementById('device_name').innerHTML = deviceName;
        }


        //--------------------------------------------------
        //Read後の処理:得られたデータの表示など行う
        //--------------------------------------------------
        ble.onRead = function(data, uuid) {
            //フォーマットに従って値を取得
            value0 = data.getInt8(0);
            value1 = data.getInt8(1);

            value = value0 + value1/100; // 小数点以下を元の数値に戻す
            value = value.toFixed(2); //小数点第2位まで強制表示

            //コンソールに値を表示
            console.log(value);

            //value_sensorの値を更新
            console.log("【sensor value update】");
            value_sensor = value;
        }


        //--------------------------------------------------
        //Reset後の処理
        //--------------------------------------------------
        ble.onReset = function() {
            //HTMLに表示
            document.getElementById('device_name').innerHTML = "No Device";
        }
    </script>
</body>
</html>

ソースコード補足
- MilkcocoaとOpenWeatherMapのAPP IDは伏せてあります
- 天気情報の場所は仮に東京駅に設定してあります
- CSSはBlueJellyのGithubからダウンロードできます

PC/スマホ

クラウドからデータ取得

image.png
Milkcocoaを使います
取り出すときはstreamオブジェクトを使います。
具体的な取り出し方はdocumentに例が載っています。
https://mlkcca.com/document/api-js.html

var stream = milkcocoa.dataStore('user').stream().size(999).sort('asc');

function loop(stocks, callback) {
  stream.next(function(err, elems) {
    stocks = stocks.concat(elems); // 結合
    if(elems.length > 0) loop(stocks, callback); // elemsが空になるまでloop()を実行
    else callback(stocks); // コールバックにstocksを渡す
  });
}

loop([], function(data) {
  // dataにすべてのデータが
  data.forEach(function(d,i){
    console.log(d);
  });
});

時間のかかる処理になるため、非同期処理として扱います。

長期グラフ表示

image.png
chart.jsを使います
常に等間隔でデータが取得できれば、普通の折れ線グラフで良いのですが、エラーや中断等で等間隔にデータが取得できないことも想定されます。今回は、必ずしも等間隔で取得できない時系列データに対し、X軸を等間隔に描画させるのがポイントです。(Excelでいうところの散布図)

アプリ

このような表示にしました
image.png
縦軸が温度、横軸が時間で、お天気APIと温度センサとその差分の3つの温度データを描画します。またテキストボックスに日時を入力してUpdateボタンを押せば、指定した区間のみの表示に変更することが可能です。

Milkcocoaからのデータ取得とchart.jsによる描画アプリの全ソースコードです

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title>Line Chart</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.20.1/moment.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.4.0/Chart.min.js"></script>
  <script src="https://cdn.mlkcca.com/v0.6.0/milkcocoa.js"></script>
    <style>
    canvas {
        -moz-user-select: none;
        -webkit-user-select: none;
        -ms-user-select: none;
    }
    </style>
</head>
<body>
    <div style="width:75%;">
        <canvas id="canvas"></canvas>
    </div>
    <p> start:<input id="textBox_start" type="text" value=""></p>
    <p>  end :<input id="textBox_end" type="text" value=""></p>
    <p><button id="button_update">Update</button></p>
    <p id="calc"> </p>

    <script>
    //---------------------------------------------
    //Grobal
    //---------------------------------------------
    var allData_ApiTemp = [];
    var allData_SensorTemp = [];
    var allData_DiffTemp = [];
    var data_ApiTemp = [];
    var data_SensorTemp = [];
    var data_DiffTemp = [];

    //---------------------------------------------
    //ClickEvent
    //---------------------------------------------
    document.getElementById("button_update").addEventListener("click", function(){
        //テキストボックスからstart_unix_timeとend_unix_timeを得る
        start_unix_time = document.getElementById("textBox_start").value;
        if(start_unix_time !="")
            start_unix_time = moment(start_unix_time).unix() * 1000;//UNIX TIMEに変換後、1000倍してmillisecondにする

        end_unix_time = document.getElementById("textBox_end").value;
        if(end_unix_time !="")
            end_unix_time = moment(end_unix_time).unix() * 1000;//UNIX TIMEに変換後、1000倍してmillisecondにする

        //配列クリア
        data_ApiTemp.length = 0;
        data_SensorTemp.length = 0;
        data_DiffTemp.length = 0;

        //allData_ApiTempから、指定範囲内のデータのみdata_ApiTempに取り入れる
        allData_ApiTemp.forEach( function(d, i ) {
                x = d.x;
                y = d.y;

                //テキストボックスのUNIX TIMEに合わせ、ミリ秒切り捨て
                x = Math.floor(x/1000)*1000;

                if(x >= start_unix_time && x <= end_unix_time)
                    data_ApiTemp.push({x, y});
        });

        //allData_SensorTempから、指定範囲内のデータのみdata_SensorTempに取り入れる
        allData_SensorTemp.forEach( function(d, i ) {
                x = d.x;
                y = d.y;

                //テキストボックスのUNIX TIMEに合わせ、ミリ秒切り捨て
                x = Math.floor(x/1000)*1000;

                if(x >= start_unix_time && x <= end_unix_time)
                    data_SensorTemp.push({x, y});
        });

        //allData_DiffTempから、指定範囲内のデータのみdata_DiffTempに取り入れる
        allData_DiffTemp.forEach( function(d, i ) {
                x = d.x;
                y = d.y;

                //テキストボックスのUNIX TIMEに合わせ、ミリ秒切り捨て
                x = Math.floor(x/1000)*1000;

                if(x >= start_unix_time && x <= end_unix_time)
                    data_DiffTemp.push({x, y});
        });

            //グラフの更新
            graph_update();

        });

    //---------------------------------------------
    //Milikcocoaからデータ取得
    //---------------------------------------------
    var milkcocoa = new MilkCocoa('*************************');//Milkcocoaで取得したIDを入力すること

    //データを999個以上取得
    var stream = milkcocoa.dataStore('data').stream().size(99).sort('asc');

    function loop(stocks, callback) {
      stream.next(function(err, elems) {
        stocks = stocks.concat(elems); // 結合
        if(elems.length > 0) loop(stocks, callback); // elemsが空になるまでloop()を実行
        else callback(stocks); // コールバックにstocksを渡す
      });
    }

    function milkcocoa_excute(callback)
    {
        loop([], function(data) {
          // dataにすべてのデータが入る
          data.forEach(function(d,i){
                console.log(d);

                //allData_ApiTempへデータを貯める
                unix_time = d.value.unix_time;
                temp_api = d.value.temp_api;
                temp_sensor = d.value.temp_sensor;
                temp_diff = temp_api - temp_sensor;

                allData_ApiTemp.push({"x":unix_time, "y":temp_api});
                allData_SensorTemp.push({"x":unix_time, "y":temp_sensor});
                allData_DiffTemp.push({"x":unix_time, "y":temp_diff});

          });

            //配列のコピー
          data_ApiTemp = allData_ApiTemp.concat();
          data_SensorTemp = allData_SensorTemp.concat();
            data_DiffTemp = allData_DiffTemp.concat();

            callback();
        });
    }


    //---------------------------------------------
    //chart.jsのグラフ設定
    //---------------------------------------------
    var config = {
            type: 'line',
            data: {
                datasets: [
                {
                 label: "API Temperature ",
                 backgroundColor: 'rgba(255, 99, 132, 0.5)',
                 borderColor: 'rgb(255, 99, 132)',
                 fill: false,
                 data: data_ApiTemp,
             },
             {
                label: "Senser Temperature ",
                backgroundColor: 'rgba(54, 162, 235, 0.5)',
                borderColor: 'rgb(54, 162, 235)',
                fill: false,
                data: data_SensorTemp,
            },
            {
             label: "Temperature difference",
             backgroundColor: 'rgba(75, 192, 192, 0.5)',
             borderColor: 'rgb(75, 192, 192)',
             fill: false,
             data: data_DiffTemp,
         }
            ]
            },

            options: {
                scales: {
                    xAxes: [{
                        type: "time",
                        time: {
                                tooltipFormat: 'll HH:mm'
                        },
                        scaleLabel: {
                                        display: true,
                                        labelString: 'date'
                                    }
                        }],
                    yAxes: [{
                                scaleLabel: {
                                    display: true,
                                    labelString: 'degrees Celsius'
                                }
                    }]
                }
            }
        };


    //---------------------------------------------
    //chart.jsのグラフ更新
    //---------------------------------------------
    function graph_update(){
        config.data.datasets[0].data = data_ApiTemp;
        config.data.datasets[1].data = data_SensorTemp;
        config.data.datasets[2].data = data_DiffTemp;
        window.myLine.update();

        //テキストボックスにtimestampを表示
        //開始date
        timestamp_string = data_ApiTemp[0].x;
        timestamp_string = moment(timestamp_string).format()
        document.getElementById("textBox_start").value = timestamp_string;

        //終了date
        timestamp_string = data_ApiTemp[data_ApiTemp.length - 1].x;
        timestamp_string = moment(timestamp_string).format()
        document.getElementById("textBox_end").value = timestamp_string;

        //平均値と標準偏差の計算
        calc();
    }

    //---------------------------------------------
    //温度差の平均値と分散
    //---------------------------------------------
    function calc(){
        var sum = 0;
        //合計値
        data_DiffTemp.forEach( function(d, i ) {
                y = d.y;
                sum = sum + y;
        });

        //平均値
        average = sum/data_DiffTemp.length;
        ave = Math.round( average*1000 )/1000;
        document.getElementById("calc").innerHTML = "Average : " + ave + " degree<br>";

        //分散
        var sigma2 = 0;
        data_DiffTemp.forEach( function(d, i ) {
                sigma2 = sigma2 + Math.pow(d.y - average, 2);
        });
        sigma2 = sigma2/data_DiffTemp.length;

        //標準偏差
        sigma = Math.sqrt(sigma2);
        threeSigma = Math.round( 3*sigma*1000 )/1000;
        document.getElementById("calc").innerHTML += "3σ : " + threeSigma + " degree";

    }


    //---------------------------------------------
    //ロード時の処理
    //---------------------------------------------
    window.onload = function() {
        //chart.js
        var ctx = document.getElementById("canvas").getContext("2d");
        window.myLine = new Chart(ctx, config);

        //milkcocoaでデータ取得後にグラフ表示
        milkcocoa_excute(graph_update);
    };
    </script>
</body>
</html>

ソースコード補足
- MilkcocoaのAPP IDは伏せてあります

結果

image.png

12/30~1/5までの約6日間を測定した結果です。
赤がお天気APIの温度データ、青がベランダで測定した温度センサによる温度データ、緑は両者の差分です。

一番最初に差があるのは、暖房の効いた家の中にずっと置いてあった温度センサによる影響です。
青が時々急峻に0°に落ちているのはセンサーデータが取れなかったときの状況を示しています。一番最後は通信エラーで落ちっ放しになっています。

これを見ると結構な違いあるのが分かります。
ほとんどのケースでAPI温度は温度センサに比べて、昼間は温度が高く、夜間は温度は低い状態になっています。

image.png
一番の原因として推測されるのは、温度センサの箱です。突然の雨風に曝されないように小さな段ボールの中に入れて観測していたことです。つまり、外装の断熱効果による影響をかなり受けたのではないかと推測されます。

段ボールは急激な暑さや急激な寒さを断熱効果で緩やかにしてくれることが分かりました。
結論としては、測定条件が甘かった お天気APIの気温が本当に正しいのか確認したら、段ボールの断熱効果のありがたみが分かったということです。

また時間があるときに、測定条件など見直してこの実験をアップデートできればと思います。ではまた!

参考リンク