モノワイヤレス TWELITEとGoogle App Scriptで作る農業向け温湿度測定・監視システム

![IMG_20230314_094933_2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3121020/7c9ba4c6-83d5-1e78-c095-3b4525b07562.jpeg)


<h1>TWELITE® ARIA-トワイライトアリア</h1>

https://mono-wireless.com/jp/products/twelite-aria/index.html

参考　TWELITE ARIA ユーザーズガイド　https://twelite.gitbook.io/mw-pug-aria/

- コイン電池ホルダ、湿度・温度センサー、磁気センサー、アンテナをワンパッケージにした小型の無線タグ
- 湿度・温度・磁気を内蔵のセンサーで読み取り、無線で送ることができる
- TWELITE ARIAのケースは防水仕様ではないため、ケースの合わせ目を防水テープなどで封をする、空気穴に通気テープを張り付ける、ねじ止め穴にOリングをいれる等の対策がある

<h6>App_ARIA</h6>

https://mono-wireless.com/jp/products/TWE-APPS/App_ARIA/index.html

- アリアアプリ（App_ARIA）は磁気・湿度・温度センサータグ TWELITE ARIA専用のアプリ
- TWELITE ARIAには本アプリがあらかじめインストールされている
- 親機、中継機を親機・中継機アプリ（App_Wings）に書き換える。親機、中継機にはMONOSTCK - モノスティックを使用すると便利
- TWELITE ARIAの初期設定の定期送信間隔は5秒。電池寿命を伸ばすには送信間隔を長くする。無線の送信間隔を変更はインタラクティブモードで行う
- LEDは相手側の端末から点灯・消灯させることが出来る
- I2Cセンサーを拡張するための端子を有する
- 代表的な電池寿命は1分に1度の定期送信 + 1分に1度磁石を近づけた場合、約2.5年
- 設定方法は、MONOSTICKによるOAT又はTWELITE R2による7Pインターフェイスを使用して行う。なお7Pインターフェイスの接続は、TWELITE R2を逆向きに接続するとTWELITE ARIAが破損するため注意する

<h6>親機の出力</h6>

https://twelite.gitbook.io/mw-pug-aria/usage/mode-selection/twelitearia-mode#no

インタラクティブモードによる設定　https://twelite.gitbook.io/mw-pug-aria/interactive-mode
- a：アプリケーションID（初期値0x67720102）※同一の周波数チャネルを複数のグループで使用することが可能です。値は32ビットで設定します。
- c：周波数チャネルの設定（初期値18）※チャネル(11～26)を選択
- x：送信出力の設定（初期値13）※１桁目は送信出力を設定、3が最強。２桁目は再送回数を設定
- t：送信間隔の設定（初期値5）※定期送信パケットの送信間隔を秒単位で設定します。1〜4095の値で指定可能。5分の場合は300に設定


<h6>TWELITE USBスティック</h6>

https://mono-wireless.com/jp/products/MoNoStick/index.html

参考　親機・中継機アプリ ユーザーズガイド　https://wings.twelite.info/

- パソコンを親機に、単体で中継機に
- 親機・中継機アプリがインストールされており、すぐに使用開始できます。設定を変更することにより、中継機として使用できます。
- 接続されたデバイスからMONOSTICK-モノスティックはシリアルポートに見えますので、データのやりとりが標準的な方法で行えるため、パソコン、タブレット、ボードPC側のアプリの開発が容易です。シリアルポート経由でコマンド文字列、ステータス文字列の送受信により動作させます

<h6>親機・中継機アプリ（App_Wings）</h6>

https://mono-wireless.com/jp/products/TWE-APPS/App_Wings/index.html

- 親機・中継機アプリ (App_Wings) は超簡単！標準アプリやパルアプリなどの TWELITE APPS やAct のパケットを受信と中継をするアプリです。
- アリアアプリからの受信メッセージ　https://wings.twelite.info/how-to-use/parent-mode/receive-message/app_aria
- 温度などのセンサーデータは103文字目から118文字目までに含まれる


<h6>中継器モードについて</h6>

https://wings.twelite.info/how-to-use/router-mode
- 中継ネットを使用するアプリのデータを1段の中継を行う場合、動作モードの値を1に設定してしてください。
- 複数回の中継を行う場合は、親機から遠くなるにつれて動作モードの設定値を大きくしてください。(設定値が昇順になっていれば設定値が飛んでもかまいません。)
- 本方式の最大中継回数は63回までです。


<h6>インタラクティブモードによる設定</h6>

https://wings.twelite.info/interactive-mode

- a：0x67720102（初期値0x67720102）※同じアプリケーションID同士のみ通信可能です。値は32ビットで任意に設定できます。通信させたい端末は全て同じアプリケーションIDに設定する必要があります。
- c：周波数チャネルの設定（初期値18）※チャネル(11～26)を選択。複数チャネルを指定した場合は チャネルアジリティにより電波干渉の回避に役立ちます。最大3チャネルまで指定可能
- x：中継時の送信出力と再送回数（初期値03）※１桁目は送信出力を設定、3が最強。２桁目は再送回数で1-9 が指定の回数、0 が再送なし
- m：動作モード（初期値0）※0は親機モード。1は中継モード。2～63は中継ネットを使用したアプリのパケットを多段中継するときに使用
- A：接続先（初期値0x00000000）※中継機モード時に静的ルーティングをするときの接続する上位段のTWELITEのSIDを指定します。近くに存在しないSIDや下位段のSIDを指定したときは不定となるので指定する場合は必ず存在する上位段のSIDを入力してください。0x00000000の時は自動で上位段を検索し、発見したTWELITEを通信を試みます。



<h6>TWELITEの通信について</h6>

- 定期的な送信を行う場合は送信間隔を長めに設定する。10台の送信機を使用する場合は送信間隔を 10*100ms = 1秒（1000ms）以上に設定
- 送信前にランダムな遅延を入れる。TWELITEの送信前の遅延やスリープ時間をランダムにすることで、複数の送信機の電波の出力タイミングの同期や通信の輻輳などの影響を少なくすることができます。 パルアプリ、キューアプリでは標準で送信間隔を最大±10％程度ブレさせる機能があります。
- デバイスIDはインタラクティブモードで割り振ることができるため、簡単に設定することができます。設定可能範囲は1～100で、最大接続台数は100台
- TWELITE毎に固有のシリアルIDが割り振られており、通信のアドレスとして使用するとができます。接続台数は無制限です。ユーザが割り振ることができないため、TWELITEを交換する際にTWELITEアプリやパソコンなどのソフトウェアの対応が必要な場合がある
参考　TWELITE最大接続数　https://mono-wireless.com/jp/tech/Programming/Tips_Max_Connection.html



<h1>TWELITEの設定</h1>


<h6>使用方法</h6>

1\. パソコンにTWELITE STAGE SDKをインストール　リンク
- TWELITEトワイライト で動作するソフトウエア（アプリ）を開発するためのライブラリやコンパイラが含まれた環境。まずはこれをインストール
- インストールディレクトリには空白文字や日本語名などの英数字以外が含まれないようにする。

2\. TWELITE STAGE APPを起動する　リンク
- TWELITE STAGE アプリ　Windows の場合：TWELITE_Stage.exe（通常版）、TWELITE_Stage_VSCode.exe（VSCode対応版）
- TWELITE RまたはMONOSTICKがUSBポートに正しく接続されていることを確認する
- MWSTAGEフォルダ内の以下のファイルをダブルクリックする。　※TWELITE_stage.exe（Windows）
- 起動するとUSBに接続されたTWELITE RまたはMONOSTICKが画面上に表示されるので、使用するデバイスを選択
- デバイスを選択するとメニュー画面が現れる

3\.TWELITE ARIAをTWELITE R2に接続し、初期設定を行う
- 接続は、TWELITE R2を逆向きに接続するとTWELITE ARIAが破損するため注意する
- インタラクティブモードを選択
- [i]をタイプして、Device IDを変更する（センサーに1～100までの識別番号を付ける）
- [t]をタイプして、送信間隔を変更する（初期値5）※定期送信パケットの送信間隔を秒単位で設定します。1〜4095の値で指定可能。5分の場合は300に設定
  （送信間隔t=300）

4\. 中継機として使用するMONOSTICKをパソコンに接続し、初期設定を行う
- パソコンのUSBにMOTOSTICKを接続
- インタラクティブモードを選択
- [m]をタイプして、動作モードを変更する（初期値0）※0は親機モード。1は中継モード。2～63は中継ネットを使用したアプリのパケットを多段中継するときに使用



<h1>Raspberry Pi の準備</h1>


 <h6>Raspberry Pi Imagerをダウンロード、SDカードを入れて起動する</h6>　https://www.raspberrypi.com/software/

- ［OSを選ぶ］をクリック、［Raspberry Pi OS (other)］＞［Raspberry Pi OS Lite (32-bit)］を選択
- ［ホスト名］：初期設定の通り raspberrypi とする
- ［SSHを有効化する］：「パスワード認証を使う」にチェックを入れる
- ［ユーザー名とパスワードを設定する］：［ユーザー名］を pi 、［パスワード］は任意に（今回はpi）
- ［Wi-Fiを設定する］：［SSID］には Raspberry Pi から接続したいWi-FiのSSIDを入力、［パスワード］には、SSIDに対応するパスワードを入力、［Wifiを使う国］は「JP」を選択
- ［ロケール設定をする］：［タイムゾーン］は「Asia/Tokyo」を選択


<h6>初期設定を行う</h6>

1\. ターミナルソフトでSSHログイン
- 使用するターミナルソフトはWindowsならTeraTerm,
- 先ほど設定した「ホスト名：raspberrypi.local」を指定してSSH接続

2\. パッケージの更新
- セキュリティアップデートもあるはずなので、必ず行っておく
```
$ sudo apt update && apt upgrade
```

3\. pythonのインストール
```
$ sudo apt-get -y install python3-dev
$ sudo apt-get -y install python3-pip
$ sudo pip3 install pyserial 
$ sudo pip3 install requests
```
確認
```
$ python --version
Python 2.7.16
$ pip3 --version
pip 18.1 from /usr/lib/python3/dist-packages/pip (python 3.7)
```
4\. USBメモリからPAL_Scriptをコピーする
- USBメモリにPAL_Scriptと作成したpythonプログラムを保存
- $ sudo fdisk -l　でusbメモリが認識されているか確認。/dev/sda1 というDeviceが表示されていれば、OK
- $ sudo mkdir /media/usb1　によりマウント先のディレクトリを作成
- $ sudo mount /dev/sda1 /media/usb1　によりUSBメモリをマウント
- $ df　によりデバイス /dev/sda1が /media/usb1 にマウントされていることが確認出来たら成功
- PAL_Scriptフォルダを/home/piにコピー
```
$ sudo cp -a /media/usb1/PAL_Script /home/pi
```
- $ sudo umount /dev/sda1　によりUSBメモリーをアンマウント

5\. pythonプログラムの起動テスト
- PAL_Script.pyを起動
- 引数：-t　MONOSTICKが使用するポート名を指定 設定項目：MONOSTICKのポート名 初期値：Windowsの場合：COM3、Linuxなどの場合：/dev/ttyUSB0
```
> cd /home/pi/PAL_Script 
> python PAL_Script.py -t /dev/ttyUSB0
```

6\. 自動起動の設定
- サービスの「設定ファイル」を作成
```
cd /usr/lib/systemd/system
sudo nano PAL_daemon.service　※ファイル名は任意
```
- 設定ファイルの中身
```
[Unit] 
Description=Sample Daemon 
[Service]
WorkingDirectory=/home/pi/PAL_Script/ 
ExecStart=python /home/pi/PAL_Script/PAL_Script.py -t /dev/ttyUSB0 
Restart=always 
Type=simple 
[Install] 
WantedBy=multi-user.target
```
- 再起動後、サービスの起動を確認
```
sudo systemctl status PAL_daemon
```
- サービスの自動起動を有効にする
```
sudo systemctl enable PAL_daemon
```

WIFI設定の変更
```
sudo raspi-config
```



<h1>センサーのゲートウェイ用のpythonプログラム</h1>

参考
- パルスクリプト　https://mono-wireless.com/jp/products/TWE-APPS/App_pal/palscript.html
- TWELITE PAL/CUE/ARIA Script　https://palscript.twelite.info/


<h6>pythonのプログラム及び実行環境のインストール</h6>

Windowsで使用する場合
1. Pythonの最新版をダウンロードしインストールする。https://www.python.org/downloads/
2. インストール時にPythonのインストール先にPathを通すこと
   
3. コマンドプロンプトを立ち上げ、下記コマンドを入力しpyserialをインストールする
   > pip install pyserial
4. スクレイピングでよく使われるライブラリーrequestsをインストールする
   > pip install requests

<h6>Linux(Ubuntu/RaspberryPi OSなど)で使用する場合</h6>

1\. ターミナルを起動し、下記コマンドでPython3をインストールする。
```
$ sudo apt update
$ sudo apt install python3
```

2\. コマンドプロンプトを立ち上げ、下記コマンドを入力しpyserial及びrequestsをインストールする
```
   $ sudo pip3 install pyserial
   $ sudo pip3 install requests
```

<h6>受信したシリアルデータを解釈するためのPythonのサンプルスクリプトをダウンロード</h6>

1\. ここからパルスクリプトをダウンロード　https://mono-wireless.com/jp/products/TWE-APPS/App_pal/palscript.html
2\. パソコンのMONOSTICKを接続し、センサーを動作させる
3\. MONOSTICKのCOMポートがCOM6の場合、ターミナルで下記コマンドを実行すると下記のようなデータが出力される。
```
> cd C:\PAL_Script 
> python PAL_Script.py -t COM6 
*** MONOWIRELESS App_PAL_Viewer 1.2.0 *** 
  *** Open COM6 *** 
Receive Time: 2021/12/17 17:14:29.859 
Logical ID: 0x01 
Serial ID: 0x810A63A4 
Power: 3410 mV 
Sensor: TWELITE-ARIA 
HALLIC: 128 
Temperature: 24.53 degC 
Humidity: 36.69 % 
…
```
- スクリプトを終了させるにはターミナルに'Ctrl+C'を入力


<h6>ゲートウェイ用プログラムの作成</h6>

1\. [PAL_Script]の中の、「Main_user.py」に実行したい処理をプログラムする。
2\. プログラムを実行する際は、「PAL_Script.py」を実行する。そこから、「Main_user.py」が呼び出される。

<h6>ゲートウェイのpythonプログラム</h6>

- センサーからデータを受信したら、データをgoogle app scriptにPOSTするだけの単純な処理
- Main_user.pyのコード
```
#!/usr/bin/env python 
# coding: UTF-8 
################################################################# 
# Copyright (C) 2017-2021 Mono Wireless Inc. All Rights Reserved.    # 
# Released under MW-SLA-*J,*E (MONO WIRELESS SOFTWARE LICENSE   # 
# AGREEMENT).                                                   # 
################################################################# 
import sys 
import datetime 
import json 
import requests 
# WONO WIRELESSのシリアル電文パーサなどのAPIのインポート 
sys.path.append('./MNLib/') 
try: 
	from apppal import AppPAL 
except ImportError: 
	print("Cannot Open MONOWireless library...") 
	sys.exit(1) 
#自作したexpireEncodaはTrueの場合そのオブジェクトを文字列で返してくれるという関数 
def expireEncoda(object): 
	if isinstance(object, datetime.datetime): 
		return object.isoformat() 
# この関数に処理したい内容を書く 
def Main(PAL=None): 
	#google app scriptでデプロイしたアプリのURLを記載する 
	url = 'https://script.google.com/macros/s/--------------------/exec' 
	# 渡された変数がAppPALクラスか確認する。 
	if isinstance(PAL, AppPAL): 
		#センサーのデータをsns_dataに格納 
		sns_data = PAL.GetDataDict() 
		#sns_dataをgoogle app scriptのアプリにPOST 
		requests.post(url, data=json.dumps(sns_data, default=expireEncoda)) 
		#送信したjsonデータを表示 
		#print(json.dumps(sns_data, default=expireEncoda))
```
- 送信されるデータ
```
{"ArriveTime": "2023-03-06T00:11:54.466748", "LogicalID": 1, "EndDeviceSID": "810A6E41", "RouterSID": "80000000", "LQI": 219, "SequenceNumber": 1593, "Sensor": 128, "PALID": 6, "PALVersion": 0, "WakeupFactor": [129, 53, 0], "EventID": [53, 1, 0], "Power": 3120, "ADC1": 1003, "HALLIC": 128, "Temperature": 23.43, "Humidity": 52.47}
```



<h1>データを受信するGoogleスプレッドシートのプログラム</h1>


<h6>GASの受信プログラム</h6>

- Google AppScriptでHTTP POSTを受け付けて、スプレッドシートに書き込む
- 各センサーのLogicalID毎にシートを自動で作成し、最終行に追記していく
- 書き込むデータは、時間、温度、湿度だけを選択

１．Googleスプレッドシートを開き、[拡張機能][App Script]をクリック
２．コードを書く
```
function doPost(e) {    //e.postDataはblob型 
  let value = JSON.parse(e.postData.getDataAsString());  // ※.getDataAsString()メソッドでJSON文字列を得ることができ、それをJSON.parseすることでJavaScriptのオブジェクトに変換 
  let addValue = [value.ArriveTime,value.Temperature,value.Humidity]; //取得したセンサーのデータのうち、"ArriveTime","Temperature","Humidity"を新たな配列に取得。スプレッドシートに書き込むデータを増やす場合はここに追加 
  let sheet_name = value.LogicalID; //jsonデータからLogiclaIDを取得 
  let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheet_name); //LogicalIDと同じシート名を指定 
  if(sheet == null){  //LogicalIDに対応するシートがない場合、新しく作成 
    let newSheet = SpreadsheetApp.getActiveSpreadsheet().insertSheet(); 
    newSheet.setName(sheet_name); //追加挿入したシートに名前を設定 
    let title = ['date','temp','humid'];  //タイトル行の配列を作成 
    newSheet.appendRow(title);  //タイトル行を書き込み 
    sheet = newSheet; 
  } 
  sheet.appendRow(addValue);  //シートの最終行に追加する 
}
```
３．[デプロイ][新しいデプロイ]をクリック、[アクセスできるユーザー]を全員とし、[デプロイ]を実行。
４．生成されたURLをコピーして、先のpythonのコードに書き込む
```
https://script.google.com/macros/s/----------------/exec
```
※このURLにアクセスすることで、GASで作成されたアプリケーションの利用が可能になる



<h6>スプレッドシートの構成</h6>

- 「設定」シート　センサー数、警報送付先のLINE token、各センサーの名前・温湿度の警報値を設定する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3121020/f78a7120-0bc0-b709-06d7-1051918f8986.png)

  
- 「Data」シート　各センサーの測定値を1時間毎に転記して保存する
- 「1,2…」シート　各センサーの測定値を5分ごとに記録。最新のデータ3日分のみ保存（グラフを作成しておく）
  ![image (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3121020/f99937e7-cf28-afbf-6033-dc07dc8fc657.png)




<h6>センサーの値を1時間ごとに転記するプログラム</h6>

- トリガーを1時間ごとに設定し、各センサーの「1,2…」シートから最新値を「Data」シートに転記する
```
function hourData(){ 
  // Spreadsheet からデータを読み込む   
  let mySpreadsheet = SpreadsheetApp.openById('----------------------------'); //TWELITEのデータが入ったスプレッドシートをIDを指定して取得 
  let settingSheet = mySpreadsheet.getSheetByName('設定');  //[設定]シートを取得 
  let dataSheet = mySpreadsheet.getSheetByName('Data'); //[Data]シートを取得 
  let sensorNo = settingSheet.getRange(1,2).getValue(); //センサー台数(セル1B)の値を取得 
  let senorName = settingSheet.getRange(17,2,sensorNo,5).getValues(); //センサー名・警報値(セル17B)からセンサーの個数分の値を取得 
  let table = new Array();  //[Data]シートに書き込むデータ配列を格納する変数を宣言 
   
  // 現在から5分前の日時を取得 
  let dayTime = new Date(); //現在日時を取得 
  table[0] = Utilities.formatDate(dayTime, 'JST', 'yyyy-MM-dd HH:mm:ss'); 
  dayTime = new Date(dayTime - (1000*60*10));  //10分前の日時 
  // 各センサーのシートから最終行を取得、10分以内なら配列に格納 
  for(let i=0;i<sensorNo;i++){ 
    let sheet = mySpreadsheet.getSheetByName(i+1);  //センサー名のワークシートを取得 
    let tempData = sheet.getRange(sheet.getLastRow(),1,1,sheet.getLastColumn()).getValues().flat();  //最終行の値を取得 
    if(tempData[0] > dayTime){ 
      table.push(tempData[1],tempData[2]); 
    }else{ 
      table.push("",""); 
    } 
  } 
  //Dataシートの最終行に追加する 
  dataSheet.appendRow(table); 
}
```



<h6>温湿度の警報をLINE notifyに送信するプログラム</h6>

- トリガーを5分ごとに設定し、各センサーの「1,2…」シートから最新値を取得、「設定」シートの警報値設定と比較し、上限・下限を超えた場合に、LINE notifyにメッセージを送信
```
function alertLine(){ 
  // Spreadsheet からデータを読み込む   
  let mySpreadsheet = SpreadsheetApp.openById('--------------------------------'); //TWELITEのデータが入ったスプレッドシートをIDを指定して取得 
  let settingSheet = mySpreadsheet.getSheetByName('設定');  //[設定]シートを取得 
  let sensorNo = settingSheet.getRange(1,2).getValue(); //センサー台数(セル1B)の値を取得 
  let alertSetting = settingSheet.getRange(17,2,sensorNo,5).getValues(); //センサー名・警報値(セル17B)からセンサーの個数分の値を取得 
  let lineToken = settingSheet.getRange(5,2,10).getValues().flat().filter(Boolean); //ラインToken(セル5B:14B)の値を取得し、flat()で一次配列化し、空要素を削除 
  let maxRow = 865; //５分データの最大データ数（３日分） 
    // 現在から5分前の日時を取得 
  let dayTime = new Date(); //現在日時を取得 
  dayTime = new Date(dayTime - (1000*60*5));  //5分前の日時 
  // 各センサーのシートから最終行を取得 
  for(let i=0;i<sensorNo;i++){ 
    let sheet = mySpreadsheet.getSheetByName(i+1);  //センサー名のワークシートを取得 
    let tempData = sheet.getRange(sheet.getLastRow(),1,1,sheet.getLastColumn()).getValues().flat();  //最終行の値を取得 
    //警報値を判定　※設定が空白の場合は無視する 
    if(tempData[0] > dayTime){ 
      if((alertSetting[i][1]!="" && tempData[1]>alertSetting[i][1])||(alertSetting[i][2]!="" && tempData[1]<alertSetting[i][2])||(alertSetting[i][3]!="" && tempData[2]>alertSetting[i][3])||(alertSetting[i][4]!="" && tempData[2]<alertSetting[i][4])){ 
        let alertTime = Utilities.formatDate(tempData[0], 'JST', 'yyyy-MM-dd HH:mm:ss'); 
        console.log(alertTime + " " + alertSetting[i][0] + "で警報が発生しています"); 
        sendPostContent(alertTime + " " + alertSetting[i][0] + "で警報が発生しています",lineToken); 
      } 
    } 
    //余分なデータ行を削除 
    let lastRow = sheet.getLastRow(); 
    if(lastRow-maxRow>0){ 
      sheet.deleteRows(2,lastRow-maxRow); 
    } 
  } 
}

// Line Notifyに通知 
function sendPostContent(content,lineToken) { 
  for(let i of lineToken){ 
    var options = { 
      "method": "post", 
      "payload" : {"message": content }, 
      "headers": {"Authorization": "Bearer " + i}     
    }; 
    UrlFetchApp.fetch("https://notify-api.line.me/api/notify", options); 
  } 
}
```



<h6>センサーの測定情報をWebページに表示するプログラム</h6>

- GASのスクリプトをデプロイし、そのURLにアクセスすると、センサーの測定値を動的にwebページに表示
- スクリプト本体
```
// htmlに出力　リクエストを受け取ると doGet が実行される 
function doGet() { 
  // Spreadsheet からデータを読み込む 
  let mySpreadsheet = SpreadsheetApp.openById('----------------------------'); //TWELITEのデータが入ったスプレッドシートをIDを指定して取得 
  let settingSheet = mySpreadsheet.getSheetByName('設定');  //[設定]シートを取得 
  let sensorNo = settingSheet.getRange(1,2).getValue(); //センサー台数(セル1B)の値を取得 
  let lineToken = settingSheet.getRange(5,2,10).getValues().flat().filter(Boolean); //ラインToken(セル5B:14B)の値を取得し、flat()で一次配列化し、空要素を削除 
  let senorName = settingSheet.getRange(17,2,sensorNo,5).getValues(); //センサー名・警報値(セル17B)からセンサーの個数分の値を取得 
  // 現在から5分前の日時を取得 
  let dayTime = new Date(); //現在日時を取得 
  dayTime = new Date(dayTime - (1000*60*5));  //5分前の日時 
  // 各センサーのシートから最終行を取得、5分以内なら配列に格納 
  let values =[]; 
  for(let i=0;i<sensorNo;i++){ 
    let sheet = mySpreadsheet.getSheetByName(i+1);  //センサー名のワークシートを取得 
    values.push(sheet.getRange(sheet.getLastRow(),1,1,sheet.getLastColumn()).getValues().flat());  //最終行の値を取得 
    values[i][0] = Utilities.formatDate(values[i][0], 'JST', 'yyyy-MM-dd HH:mm:ss');  //日時のフォーマットを変更 
    if(values[i][0] < dayTime){ //センサーの最新値の日時が5分前以降か判定 
      values[i][1]='no data';  //5分より前の場合は値を「no data」とする 
      values[i][2]='no data'; 
    } 
  } 
    
  // ホームページに表示するデータを格納する配列を作成 
  let table = new Array();   
  senorName.forEach(function(senorName_element, i){ //　各センサー名・警報値と各センサー値の配列を結合 
    if(values[i]){ 
      table[i] = senorName_element.concat(values[i]); 
    } 
  }); 
  table = table.map(elm => [elm[5],elm[0],elm[6],elm[7],elm[1],elm[2],elm[3],elm[4]]);   //列の順序を入れ替え 
  console.log(table); 
  table = table.map(function(x) { //日時の列を削除 
    return x.splice(1,7) 
  }); 
  // テンプレートを使ってHTML文書を作って return 
  let template = HtmlService.createTemplateFromFile("twelite"); 
  template.links = table; // こうしておくとテンプレートの方で links という変数に値が入った状態で使える 
  return template.evaluate(); 
}
```
- htmlページ
```
<!DOCTYPE html> 
<html> 
  <head> 
    <base target="_top"> 
  </head> 
  <body> 
    <table border="1"> 
      <tr><th>センサー</th><th>温度</th><th>湿度</th><th>温度上限</th><th>温度下限</th><th>湿度上限</th><th>湿度下限</th></tr> 
      <? for(let i = 0; i < links.length; i++){ ?> 
      <? let link = links[i]; ?> 
      <tr><td><?= link[0] ?></td><td><?= link[1] ?></td><td><?= link[2] ?></td><td><?= link[3] ?></td><td><?= link[4] ?></td><td><?= link[5] ?></td><td><?= link[6] ?></td></tr> 
      <? } ?> 
    </table> 
  </body> 
</html>
```



<h6>データを表示するWebページ</h6>

- Google DriveでGoogleサイトを新規作成
- 上でGASにより作成した測定値のWebページを埋め込む
- 「1,2…」シートのグラフを埋め込む
- 詳細グラフとして、「Data」シートのデータをGoogleのBIサービス Looker Studioに読み込み任意の期間表示ができるように設定し、リンクを貼り付ける

![Screenshot_2023-03-17-21-02-03-449_com.android.chrome_2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3121020/464c6181-9f89-4791-7ddb-4eefef17e3f6.jpeg)

![Screenshot_2023-03-17-21-02-53-593_com.android.chrome_2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3121020/a61885fa-e722-e70f-233c-2292e1fa13f3.jpeg)