Grafanaで温湿度IoTをグラフ化する

せっかくGrafanaがわかってきたので、グラフ化するデータをちょっと増やそうと思います。
今回グラフ化するのは、温湿度です。2つの情報源を扱います。

* Xiaomi Mijia温湿度計

　以前以下の記事を投稿し、定期的に室内の温度・湿度を記録しました。今回もそれを使います。
　　[Xiaomi Mijia 温湿度計 をIoTデバイスとして使う](https://qiita.com/poruruba/items/d16b58dfb45e513111d6)

* 気象庁の天気予報情報

　これも以前、以下の記事を投稿し、天気予報情報を取得しました。今回はこのうち、最高気温・最低気温・降水確率を使います。
　　[LINE Beaconを自宅に住まわせる](https://qiita.com/poruruba/items/a28476ad53b2232c789d)


前者は、刻々と変わる室内温度を記録することで、昼と夜の違い、人が部屋にいる場合といない場合の違いが如実に分かります。
後者は、部屋の温度と連動するのに加えて、雨雲が通り過ぎていくのがなんとなくわかります。

そんなことより、グラフを見ていただければすぐわかります。
まず前者の室内の温湿度です。
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/4ebc1998-b434-a977-1909-69e2d8d2e843.png)

今度は、最高気温・最低気温・降水確率です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/69b53d2e-acb7-43c2-eeff-4c5d35de62fc.png)

# Xiaomi Mijia温湿度計で温度と湿度を記録する

まずは記録するデータベースのテーブルを作成します。
テーブル名はとりあえず「envdata」としました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/80b3d067-8d8b-1385-41a5-31a33781916b.png)

次に温湿度の取得のスクリプトを作成します。
実際には、以下の記事の焼き直しです。
　[Xiaomi Mijia 温湿度計 をIoTデバイスとして使う](https://qiita.com/poruruba/items/d16b58dfb45e513111d6)

以下のnpmモジュールを使っています。

* noble
* xiaomi-gap-parser
* mysql
* dotenv

```js:index.js
var noble = require('noble');
var xiaomi = require('xiaomi-gap-parser');
var mysql = require('mysql');
require('dotenv').config();

const DB_HOST = process.env.DB_HOST || 【MySQLサーバのホスト名】;
const DB_PORT = process.env.DB_PORT || 【MySQLサーバのポート番号】;
const DB_USER = process.env.DB_USER || 【MySQLサーバのユーザ名】;
const UB_PASSWORD = process.env.DB_PASSWORD || 【MySQLサーバのパスワード】;
const DB_NAME = process.env.DB_NAME || 【データベース名】;
const DB_TABLE = process.env.DB_TABLE || 【テーブル名】;

var conn = mysql.createConnection({
    host : DB_HOST,
    port : DB_PORT,
    user : DB_USER,
    password : UB_PASSWORD,
    database : DB_NAME
});

const SCAN_DURATION = process.env.SCAN_DURATION ? parseInt(process.env.SCAN_DURATION) : 30000;
const SCAN_INTERVAL = process.env.SCAN_INTERVAL ? parseInt(process.env.SCAN_INTERVAL) : 600000;;

function wait_async(timeout){
	return new Promise((resolve, reject) =>{
		setTimeout(resolve, timeout);
	});
}

noble.on('stateChange', async function(state) {
    console.log('stateChange: ' + state);
    if (state === 'poweredOn') {
//        do{
            console.log('Start Scanning');
            noble.startScanning(["fe95"]);
            await wait_async(SCAN_DURATION);
            console.log('Stop Scanning');
            noble.stopScanning();
//            await wait_async(SCAN_INTERVAL);
            process.exit();
//        }while(true);
    } else {
        noble.stopScanning();
        process.exit();
    }
});

noble.on('discover', async (peripheral) => {
    var serviceData = peripheral.advertisement.serviceData;
    if (serviceData && serviceData.length) {
        for (var i in serviceData) {
            if( serviceData[i].uuid == 'fe95' ){
                var data = serviceData[i].data;
                var mijia = xiaomi.readServiceData(data);
                console.log(mijia);

				var now = new Date();
				
				var value = {
					mac: mijia.mac,
					tmp: mijia.event.data.tmp,
					hum: mijia.event.data.hum,
					created_at: now.getTime(),
				};
            
                noble.stopScanning();

                await insert_db(value);
            }
        }
    }
});

async function insert_db(value){
    return new Promise((resolve, reject) =>{
        conn.connect((err) => {
            if(err){
                console.error('error connecting: ' + err.stack);
                return reject(err);
            }

            if( !value.tmp || !value.hum )
                return resolve({result: 'NG'});

            try {
                var insert_str = 'INSERT INTO ' + DB_TABLE + " (type, mac, tmp, hum, created_at) VALUES ('mijia', '" + value.mac + "', " + value.tmp + ", " + value.hum + ", " + value.created_at + ");";
                console.log(insert_str);
                conn.query(insert_str);
                conn.end();
            }catch( err ){
                console.log(err);
                return reject(err);
            }

            resolve({result: 'OK'});
        });
    })
}
```

以下は、環境に合わせて変更してください。

【MySQLサーバのホスト名】
【MySQLサーバのポート番号】
【MySQLサーバのユーザ名】
【MySQLサーバのパスワード】
【データベース名】
【テーブル名】

あとは、シェルスクリプトを作ってCron化します。そこらへんは、以下の以前の記事と同じです。

[GrafanaでQiitaのView数を眺める](https://qiita.com/poruruba/items/ae6526f902b1945e594e)

```
#!/bin/sh

cd /home/XXXX/projects/node/cron_mijia
/home/XXXX/.nvm/versions/node/v8.12.0/bin/node index.js
```

```
> chmod uog+x index.sh
```

以下のコマンドでエディタを立ち上げ、

```
crontab -e
```

以下を書き込みました。要は、10分ごとに起動します。

```
0,10,20,30,40,50 * * * * /home/XXXX/projects/node/cron_mijia/index.sh
```


# 天気予報情報を取得する

毎度のように、データベースのテーブルを作成します。
テーブル名はとりあえず「weather」としました。

 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/2c8fb804-9331-69d6-9849-73ffa328a956.png)

それではスクリプトで天気予報情報を取得します。
取得する都市は横浜にしています。

以下のnpmモジュールを使っています。

* moment
* node-fetch
* mysql
* dotenv

```js:index.js
const fetch = require('node-fetch');
var mysql = require('mysql');
const moment = require('moment');
require('dotenv').config();

const DB_HOST = process.env.DB_HOST || 【MySQLサーバのホスト名】;
const DB_PORT = process.env.DB_PORT || 【MySQLサーバのポート番号】;
const DB_USER = process.env.DB_USER || 【MySQLサーバのユーザ名】;
const UB_PASSWORD = process.env.DB_PASSWORD || 【MySQLサーバのパスワード】;
const DB_NAME = process.env.DB_NAME || 【データベース名】;
const DB_TABLE = process.env.DB_TABLE || 【テーブル名】;

var conn = mysql.createConnection({
    host : DB_HOST,
    port : DB_PORT,
    user : DB_USER,
    password : UB_PASSWORD,
    database : DB_NAME
});

/* location: 13:東京、14:神奈川 */
function do_get_weather(location){
  return fetch('https://www.drk7.jp/weather/json/' + location + '.js', {
      method : 'GET'
  })
  .then((response) => {
      return response.text();
  })
  .then(text =>{
      text = text.trim();
      if( text.startsWith('drk7jpweather.callback(') )
          text = text.slice(23, -2);
      return JSON.parse(text);
  });
}

do_get_weather(14)
.then(async (json) =>{
  var now = moment();
  var info = json.pref.area['東部'].info;
  var weather = null;
  var date = null;
  for( var i = 0 ; i < info.length ; i++ ){
    date = moment(info[i].date, "YYYY/MM/DD");
    if( now.diff(date, 'days') == 0 ){
      weather = info[i];
      break;
    }
  }
  if( weather == null )
    return;

  var value = {
    fall_0: weather.rainfallchance.period[0].content,
    fall_6: weather.rainfallchance.period[1].content,
    fall_12: weather.rainfallchance.period[2].content,
    fall_18: weather.rainfallchance.period[3].content,
    tmp_high: weather.temperature.range[0].content,
    tmp_low: weather.temperature.range[1].content,
    moment: date,
    created_at: now.valueOf()
  };

  await insert_db(value);
})

async function insert_db(value){
  return new Promise((resolve, reject) =>{
      conn.connect((err) => {
          if(err){
              console.error('error connecting: ' + err.stack);
              return reject(err);
          }

          try {
            var date = value.moment;
            date.set('hour', 0);
            var insert_str = 'INSERT INTO ' + DB_TABLE + " (type, fall, created_at) VALUES ('fall', " + value.fall_0 + ", " + date.valueOf() + ");";
            console.log(insert_str);
            conn.query(insert_str);
            date.set('hour', 6);
            var insert_str = 'INSERT INTO ' + DB_TABLE + " (type, fall, created_at) VALUES ('fall', " + value.fall_6 + ", " + date.valueOf() + ");";
            console.log(insert_str);
            conn.query(insert_str);
            date.set('hour', 12);
            var insert_str = 'INSERT INTO ' + DB_TABLE + " (type, fall, created_at) VALUES ('fall', " + value.fall_12 + ", " + date.valueOf() + ");";
            console.log(insert_str);
            conn.query(insert_str);
            date.set('hour', 18);
            var insert_str = 'INSERT INTO ' + DB_TABLE + " (type, fall, created_at) VALUES ('fall', " + value.fall_18 + ", " + date.valueOf() + ");";
            console.log(insert_str);
            conn.query(insert_str);

            date.set('hour', 0);
            var insert_str = 'INSERT INTO ' + DB_TABLE + " (type, tmp_high, tmp_low, created_at) VALUES ('tmp', " + value.tmp_high + ", " + value.tmp_low + ", " + date.valueOf() + ");";
            console.log(insert_str);
            conn.query(insert_str);

            conn.end();
          }catch( err ){
              console.log(err);
              return reject(err);
          }

          resolve({result: 'OK'});
      });
  })
}
```

環境に合わせて以下を変更してください。

【MySQLサーバのホスト名】
【MySQLサーバのポート番号】
【MySQLサーバのユーザ名】
【MySQLサーバのパスワード】
【データベース名】
【テーブル名】

何をしているかというと、毎日深夜1時に取得することを前提に、その日の最高気温・最低気温・降水確率を取得します。
降水確率は、0～6時、6～12時、12～18時、18～24時までの4つ分がありますので、それぞれをその日の0時、6時、12時、18時のエポック時刻とともに別のrowとして記録します。そうすることで、グラフ化したときにそれが時系列に並んで表示されることになります。


毎度のように、シェルスクリプト化して、chmodして、Cronに登録します。

```
#!/bin/sh

cd /home/XXXX/projects/node/cron_weather
/home/XXXX/.nvm/versions/node/v8.12.0/bin/node index.js
```

毎日深夜1時に取得するようにしました。

```
0 1 * * * /home/XXXX/projects/node/cron_weather/index.sh
```

あとはデータが蓄積されるまで、気長に待ちましょう。

# Grafanaに登録する

それではグラフを作っていきましょう。
まずはデータソースとしてMySQLを設定するのですが、以前の記事で作成したQiitaのデータベースと同じで、テーブルのみ別にしているだけであれば、それをそのまま使うことができます。

# 温湿度計

ダッシュボードを開くか、新規に作成します。
そして新規にPanelを作成します。
Queryにはさきほどのデータソースを選択します。FROMにはenvdataを選択します。

作成するグラフは、温度と湿度の2つです。
最初はAだけあると思いますが、それを温度にして、もう一つ追加してBとして湿度を割り当てます。Add Queryボタンを押下すれば追加できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/3f0e2d92-83b8-6228-c0fe-2e0e96ba3b98.png)

温度と湿度では、単位が違うので、左側のY軸を℃、右側のY軸を%Hとしたいと思います。
表示されているグラフの左下にある2個のグラフの線のうち右側をクリックするとポップアップが表示されます。
Y-Axisタブを選択し、Use right Y-AxisをOnにします。そうするとそのグラフの線が右側に移ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/2a1adfaa-99c9-0422-bec8-46c9b95fffed.png)

Visualizationです。
Y軸に単位を入れたいのですが、すでにテンプレートがあるのでそれを選択します。Unitのところです。
また、Labelには日本語で入れておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/65ab16b1-ac81-a3f2-fc33-60d1daea640d.png)

Generalでは、適当にTitleを決めて入力します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/466f574f-c00c-b39e-8803-a76160330ec2.png)

最後に保存して完成です。

# 天気予報情報

天気予報情報もほぼ同じです。
Queryにはさきほどのデータソースを選択し、FROMにはweatherを選択します。
降水確率・最高気温・最低気温の3つのグラフなので、それぞれA、B、Cを割り当てました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/a9fa5ae9-23c6-98d4-5c86-b04716a961c8.png)

最高気温と最低気温を右側に移動しました。
降水確率のグラフをFillにして、斜め線ではなく階段状にしています。それぞれ、Line:fill、Staircase line:trueの部分です。
Unit、Labelを指定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/9ca64ebb-1f3f-cb56-ed85-7171f2348e74.png)

タイトルを決めます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/3ebd1ec9-d043-64ab-f249-afc5b2e09cc0.png)

保存して完成です！

以上