実験・製作関連実験関連

Switchbotで温度・湿度を測り、Googleスプレッドシートに記録してWEBに埋め込む方法

SwitchBot 実験・製作関連
スポンサーリンク

ベランダとルーフバルコニーの温度をモニタリングしたい!!!!

ということで、以前はマイコンだとかArduinoだとかRaspberryPIだとか、電子工作とプログラミングで温度を測ったりしていたのですが、時代は流れまして、スマート家電だかIoTだかなんだかよくわかりませんが、とりあえず温度とかの計測はずいぶんと簡単になったようです。

以下、温度計測の話が続いて、サボテンの話は出てきませんのでご注意ください。

ちなみに過去の似たような記事はこちら(過去の産物で現在はもう皆動いていないですけども)。

というわけで、新時代で使うのはこちら。

SwitchBot 温湿度計

このSwitchBot温湿度計は、単体で、Bluetoothでスマホと繋がって、専用のアプリから温度・湿度を見ることができるというものです。通信がBluetoothなので、外出先などの遠隔地からは温度・湿度を見ることはできない。Bluetoothは見通しがよければ120mまでとあったけども本当かどうか微妙(class1なのかな?)。我が家調べでは、窓1枚と壁1個で距離10mぐらいでいけたりいけなかったりでした(保証できません)。

濡れないようにジップロックに入れました。これをしてしまうと湿度が正確じゃなくなる気はしますが、私にとって湿度はどうでもいいので妥協しました。

電源は単4電池2本です。スマホの専用アプリからの見た目は↓のようなかんじ。Y軸のmin/maxが固定できないのと線の色とグラフの罫線の色が同じで、個人的に非常に見にくいのですが(どこかで設定できるのか?)、とりあえず温度グラフを見ることができます。

SwitchBot ミニハブ

こちらは、wifi(2.4GHz限定)に接続できるハブというやつで、このハブと温湿度計を接続させて(複数可)、ハブがwifiにつなげることで、外出先などの遠隔地から専用のアプリで温度・湿度を見ることができるようになる。また、発展的な使い方として、例えば温度が特定の温度になったときに、ヒーターやファンの電源を入れるなどの応用もできるようになります(むしろこっちが本命の使い方なのかも)。

USBのアダプタ?は付いていなかったので注意。

電源はUSB端子のやつ。スマホでアクセスするとこんな感じになります。雲のマークがクラウドを表していて、wifi経由で見れますよ~的なマークだと思います。

ここらへんのSwitchBot+ハブの使い方については、りとまんさんのyoutubeにとてもわかり易い説明がありましたのでURL置かせていただきます。https://youtu.be/BNmbyZbz8XY

ということで、便利ですなぁと思いながらいろいろいじってみたのですが、いまいちこの専用アプリの表示が見にくくて、さらにスマホ以外からも見たい、例えばこのブログに表示したいなぁと思っていたところSwitchBot APIなるものがあることが判明。

このAPIとやらを使えば、データを取り出して色々できるっぽい。

ということでこのAPIの使い方をいろいろなページで検索したところ↓のような流れで目的が達成できそうということが判明。

  1. スマホのSwitchBotアプリから、アクセストークンを取得。
  2. アクセストークンを用いて、Googleスプレッドシートに付加したスクリプトから温度・湿度データを取得し、5分単位で温度・湿度を保存していく。
  3. WEB等からGoogleスプレッドシートにリクエストを飛ばして、JSONで保存された温度・湿度データを取得し、Chart.jsとかGoogle Chartとかで好きに表示。

①スマホのSwitchBotアプリから、アクセストークンを取得

スマホの専用アプリのメニューから「プロフィール」→「設定」→「アプリバージョン」を連打。そうすると「開発者向けオプション」というのが出てくるのでクリック。「トークンを取得」をクリックするとアクセストークンが表示される。結構長めの文字列。これをメモっておく。

②アクセストークンを用いて、Googleスプレッドシートに付加したスクリプトから温度・湿度データを取得し、5分単位で温度・湿度を保存していく

Googleスプレッドシートのスクリプト(「拡張機能」→「Apps Script」)に下記のコードを書いて、getJSON_SwitchBotHubMini()をトリガーを時間ベースで設定して5分単位で実行にする。そうすると、5分単位でSwitchBotにアクセスして、その時の温度と湿度を取得してスプレッドシートに書いてくれる。

なお、getJSON_SwitchBotHubMini()内のheader=というところに、上述のトークンを書く必要があるのだが、直接コードに書くのはセキュリティ的にNGなので、スクリプトの左メニューの「プロジェクトの設定」から「スクリプト プロパティ」というところに書いて引用する。

ちなみに、温度と湿度の取得には、SwitchBotのデバイスIDというのが必要なのですが、それを毎度APIで取得してもいいのですが、それを5分単位でやるとアクセスコストが無駄にかかるので、事前に調べてコードに直打ちするのがいいような気はします(コメントアウトされている部分)。

そんでさらに、WEB等からのリクエストを受け取ってデータを返せるように、シートの内容を吐き出すようにWEBアプリケーションdoGet関数部分)として公開する。。

function doGet(e) {

  let responseText;
  let out = ContentService.createTextOutput();

  const sheetName = e.parameter.sheet  //シート名
  const rows = e.parameter.rows     //取得データ数

  if (!sheetName) return;
  if (!rows) return;
  const sh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName)

  let data

  //範囲指定がなければすべてのデータを取得する
  if (sh.getLastRow() - rows > 1) {
    data = sh.getRange(sh.getLastRow() - rows, 1, sh.getLastRow(), sh.getLastColumn()).getValues()
  } else {
    data = sh.getRange(1, 1, sh.getLastRow(), sh.getLastColumn()).getValues()
  }

  //1列目にデータが無いデータは除外
  data = data.filter(elm => elm[0] != "")

  let value = [];
  data.forEach((row) => {
    value.push({"time":row[2], "temp":row[3]});
  });

  let result = { data: value }
  let callback = e.parameter.callback;

  if (callback) {
    console.log(callback)
    responseText = callback + "(" + JSON.stringify(result) + ")";
    //Mime Typeをapplication/javascriptに設定
    out.setMimeType(ContentService.MimeType.JAVASCRIPT);

  } else {
    responseText = JSON.stringify(result);
    //Mime Typeをapplication/jsonに設定
    out.setMimeType(ContentService.MimeType.JSON);
  }

  out.setContent(responseText);
  return out;
}

function getJSON_SwitchBotHubMini() {

  // トークンを設定(直接書くのはセキュリティ的によろしくないので、プロジェクトの設定からスクリプトプロパティで設定しておく)
  var headers = { "Authorization": PropertiesService.getScriptProperties().getProperty("token") };

  var options = {
    "headers": headers,
  }

  // 取得したい温湿度デバイスの配列
  var deviceList = [];

  // デバイスの名前とIDを動的に取得する場合(APIコストが毎度かかるのですべきではない)
  // var url1 = "https://api.switch-bot.com/v1.0/devices"; 
  // var response = UrlFetchApp.fetch(url1,options);
  // var json=JSON.parse(response.getContentText());
  // var deviceList = [];
  // for(i=0;i<json['body']['deviceList'].length;i++){
  //   if (json['body']['deviceList'][i]["deviceType"] == "Meter"){
  //     deviceList.push({ "name": json['body']['deviceList'][i].deviceName, "id": json['body']['deviceList'][i].deviceId }); 
  //   }
  // }

  // デバイス名とデバイスIDを事前に調べて入れる場合
  deviceList.push({ "name": "温湿度計 ルーフバルコニー", "id": 【デバイスのID名1】 });
  deviceList.push({ "name": "温湿度計 ベランダ", "id": 【デバイスのID名2】 });

  //デバイスの数だけデータの取得を繰り返し
  deviceList.forEach((deviceID) => {
    var url2 = "https://api.switch-bot.com/v1.0/devices/" + deviceID.id + "/status";
    var data = UrlFetchApp.fetch(url2, options);
    var datajson = JSON.parse(data.getContentText());
    var temp = datajson['body']['temperature']
    var rhumidity = datajson['body']['humidity']

    var date = new Date();

    // 書き込むシート名は「yyyy_デバイスID」、書き込まれる行数は、5分単位で1年で288 * 365 = 105120行になり、シートの最大行以内。
    var sheet = set_sheet(Utilities.formatDate(date, 'JST', 'yyyy') + "_" + deviceID.id)

    // データ入力
    sheet.appendRow([deviceID.name, deviceID.id, date, temp, rhumidity]);
  });
}

// シートが存在しなければシート作成
function set_sheet(name) {
  //同じ名前のシートがなければ作成
  var sheet = SpreadsheetApp.getActive().getSheetByName(name)
  if (sheet)
    return sheet

  sheet = SpreadsheetApp.getActiveSpreadsheet().insertSheet();
  sheet.setName(name);
  return sheet;
}

③WEB等からGoogleスプレッドシートにリクエストを飛ばして、JSONで保存された温度・湿度データを取得し、Chart.jsとかGoogle Chartとかで好きに表示。

リクエストのやり方はcurlでもPHPでもPythonでも何でもいいんですが、とりあえずjavascriptのfetchを使って、表示にChart.jsとか使うとこんな感じ。

<script src="https://cdn.jsdelivr.net/npm/chart.js@3.5.0/dist/chart.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/locale/ja.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-moment@1.0.0"></script>

<canvas id="myChart1"></canvas>
<canvas id="myChart2"></canvas>
async function fetchAsync(url, id, label, color) {

  try {
    const response = await fetch(url);
    if (response.status !== 200) {
      // 200以外ならばエラーメッセージを投げる
      throw `response.status = ${response.status}, response.statusText = ${response.statusText}`;
    }

    const jsondata = await response.json();

    let labels = [];
    let values = [];
    for (var item in jsondata.data) {
      labels.push(jsondata.data[item].time);
      values.push(jsondata.data[item].temp);
    }

    const ctx = document.getElementById(id);

    new Chart(ctx, {
      type: 'line',
      data: {
        labels: labels,
        datasets: [{
          label: label,
          data: values,
          borderColor: color,
          pointStyle: 'circle',
          pointRadius: 2,
        }]
      },
      options: {
        plugins: {
          legend: {
            display: true
          }
        },
        scales: {
          x: {
            scaleLabel: {
              display: true,
              labelString: '時間'
            },
            type: 'time',
            time: {
              parser: 'YYYY-MM-DDTHH:mm:ss.SSSZ',
              unit: 'hour',
              stepSize: 1,
              displayFormats: {
                'hour': 'MM/DD H時'
              }
            }
          },
          y: {
            min: -10,
            max: 50,
            grid: {
              color: function (context) {
                if (context.tick.value > 0) {
                  return '#E5E5E5';
                } else if (context.tick.value < 0) {
                  return '#E5E5E5';
                }
                return '#000000';
              },
            },
          }
        }
      }
    });
  }
  catch (err) {
    console.log("err: " + err);
  }
}
var dt = new Date();
var y = dt.getFullYear();
fetchAsync('https://script.google.com/macros/s/スプレッドシートのID/exec?sheet=' + y + '_【デバイスのID名1】&rows=288', 'myChart1', 'ルーフバルコニー温度', 'rgba(0, 0, 255, 0.5)');
fetchAsync('https://script.google.com/macros/s/スプレッドシートのID/exec?sheet=' + y + '_【デバイスのID名2】&rows=288', 'myChart2', 'ベランダ温度', 'rgba(255, 0, 0, 0.5)');

完成

ということで最終的に完成したグラフが以下。とりあえず現在時刻から約24時間前までの温度を表示します。画像ではなくリアルタイム描画で、マウスオーバーでツールチップが表示されるはずです。

ルーフバルコニー温度(ページリロードでリアルタイム更新)

ベランダ温度(ページリロードでリアルタイム更新)

ということで以上、自分用の備忘録でした。

コメント

  1. ふるん より:

    うちの狭いベランダもこれ3つ使ってます。置き場所でほんと変わるところです。長らく使っていると電池入れの中が錆びてくるのが難点でしたが最近防水タイプがでたので買い換えたところです。ベランダ南側直射のとこは吹き曝しでも夏場は50°超えますね。

タイトルとURLをコピーしました