体温入力の自動化

毎日の体温をエクセルシートの手入力するのがたるいので、自動化をしてみました。
ハード構成、サービスとの連携実装をしていきます。

#目次
[1. はじめに](#1-はじめに)
[2. 構成検討](#2-構成検討)
[3. 体温計作製](#3-体温計作製)
[4. gas](#4-gas)
[5. line連携](#5-line連携)
[6. excel連携](#6-excel連携)
[7. おわりに](#7-おわりに)

# 1. はじめに
最近コロナ禍において、体温を記録するニーズが急激に高まりました。
指定のエクセルシートに自宅で測定してきた体温を手入力する、というケースにおいて、
「出社前の30分前に測定した自分の体温を忘れる」
「指定のエクセルシートを開くのがめんどくさい」
となってしまうため、ついつい記録漏れが発生してしまいます。

体温測定は仕方ないものとして、入力を自動的にできる方法を考えることにしました。
すなわち、要件は
「測定した体温を指定のエクセルシートへ人の手を介さずに入力する。」
とします。


# 2. 構成検討
構成の検討をしていきます。

● 体温測定
短時間で測定できることが望ましいので、非接触の赤外温度計を用いることにします。
MLX90164というサーモパイルユニットを用います。

● マイコン
測定した体温をディスプレイで表示し、wifiで送信するしたいので、
M5StickCを使います。

● クラウドサービス
GASを使って、Google Spreadsheetに体温と日付データを入力することにします。
入力忘れの通知とLineから体温を手動入力できるように、Line devolopersとGASの連携もすることにします。

● エクセルシートへのデータ入力
winPCから入力したいエクセルファイルをタスクスケジューラで定期実行し、
起動毎にパワークエリでSpreadSheet上のデータを取りに行くような形にします。
データをエクセル側に転記し、日付参照して所望のセルに体温が反映されるようにします。
業務のジャマにならないように、昼休みの時間をタスクスケジューラの起動時間にしておきます。

まとめると下記のような構成図です。
ｸﾗｳﾄﾞ側は無課金の範疇内でやります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/265266a6-c907-4b08-156c-ca92322e1dd1.png" width=75%>


# 3. 体温計作製
下記のようなディスプレイを作りました。
M5stickCは正面メインのボタン（M5記載）とサイドのボタンの2つがあります。
・M5ボタンで体温の測定開始する。
・体温測定後、サイドボタンでSpreadSheetへデータ送信
・複数の人が使用することを想定し、未入力数秒で待機状態に遷移し、
　待機状態でサイドボタンを押すとID切り替えをする。
とするような動きにします。
idの切替が視覚的にわかりやすいように、画像id固有の画像表示を入れました。
idが変わると画像が切り替わるようにします。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/42339ab7-5a0f-082e-0408-c8ea20add57a.png" width=75%>

MLX90614を両面ユニバーサル基板に実装し、簡易HATのような形にします。
純正のHATを使ったほうが見た目スッキリですが、高いのでケチって基板実装しました。
作りは荒いけど機能は変わらないのできにしない。


https://www.switch-science.com/catalog/6067/

また、MLX90614は入射角が50度くらいと広いので、外乱の影響を意図せず含めてしまいがちです。
指向性のある仕様のセンサを使ってもいいですが少し高くなるので、今回はタピオカストローにアルミホイルを巻き、体表にセンサをくっつける形で測定するような使い方にすることにしました。
接着剤で強引にくっつけてます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/dc0b5ec6-ed2a-9b77-51a9-07a8bff89dd1.png" width=75%>



M5Stickの動作はAruduinoで記述します。
下記のような感じです。
GASのデプロイID入力がありますが、それは次項にて。

<details><summary>ソースコードを表示（折りたたみ）</summary><div>

```php
#include "FS.h"
#include "SPIFFS.h"
#include "M5StickC.h"
#include "M5Display.h"
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <Adafruit_MLX90614.h>
#include "esp_deep_sleep.h"
#include "esp_system.h"
#include "time.h" //時刻取得用
#include <Wire.h>
#include "AXP192.h"//m5の電源管理ic

//ｻｰﾓﾊﾟｲﾙ設定
Adafruit_MLX90614 mlx = Adafruit_MLX90614();

//ピン設定
int buttun_A = 37;
int buttun_B = 39;
int led = 10;
int id=0;
int scl=26;
int sda=0;

//ﾎﾞﾀﾝ割付
int curr_A;
int last_A;
int curr_B;
int last_B;
bool flg;

//wifi設定
const char* ssid; //無線ルーターのssidを入力
const char* password; //無線ルーターのパスワードを入力
char* ssid1 = "[wifiのssid入力]"; //無線ルーターのssidを入力
char* pass1 = "[wifiのpass入力]"; //無線ルーターのパスワードを入力
char* ssid2 = "[予備用wifiのssid入力]"; //無線ルーターのssidを入力
char* pass2 = "[予備用wifiのpass入力]"; //無線ルーターのパスワードを入力
//ボタン押しながら起動で予備用wifiモードで起動するようにしておく。

//GASの情報
const char* server = "script.google.com";
const char* key ="[GASのデプロイしたidを入力]";
String url; //url入力用の変数
String adress = "https://script.google.com/macros/s/";  //googlescript web appのurl

//変数設定用
double temp;//体温
double ambient;//外気温
double offset;//実測からの補正値
double V_bat;//バッテリ電圧
double I_bat;//バッテリ電流
String id_name;
long count_timer;
long curr_time;//現在時刻格納用
long past_time;//ﾎﾞﾀﾝ押されてからの時間カウンタ
long sleep_time =30*1000; //スリープ用タイマ msec
long temp_time;
long buf_time1;
long count;//リセット用
bool send_flg;
int wifi_mode=0;//初期設定 0で家のwifi,1でスマホテザリングモード。


//bmp転送用　https://wak-tech.com/archives/1799
uint16_t read16(fs::File &f) {
  uint16_t result;
  ((uint8_t *)&result)[0] = f.read(); // LSB
  ((uint8_t *)&result)[1] = f.read(); // MSB
  return result;
}

uint32_t read32(fs::File &f) {
  uint32_t result;
  ((uint8_t *)&result)[0] = f.read(); // LSB
  ((uint8_t *)&result)[1] = f.read();
  ((uint8_t *)&result)[2] = f.read();
  ((uint8_t *)&result)[3] = f.read(); // MSB
  return result;
}

//bmpを表示する関数
void drawBmpFile(fs::FS &fs, const char *path, uint16_t x, uint16_t y) {
  if ((x >= 160) || (y >= 80)) return;

  // Open requested file on SD card
  File bmpFS = fs.open(path, "r");

  if (!bmpFS) {
    Serial.print("File not found");
    return;
  }

  uint32_t seekOffset;
  uint16_t w, h, row, col;
  uint8_t  r, g, b;

  uint32_t startTime = millis();

  if (read16(bmpFS) == 0x4D42) {
    read32(bmpFS);
    read32(bmpFS);
    seekOffset = read32(bmpFS);
    read32(bmpFS);
    w = read32(bmpFS);
    h = read32(bmpFS);

    if ((read16(bmpFS) == 1) && (read16(bmpFS) == 24) && (read32(bmpFS) == 0)) {
      y += h - 1;

      M5.Lcd.setSwapBytes(true);
      bmpFS.seek(seekOffset);

      uint16_t padding = (4 - ((w * 3) & 3)) & 3;
      uint8_t lineBuffer[w * 3 + padding];

      for (row = 0; row < h; row++) {
        bmpFS.read(lineBuffer, sizeof(lineBuffer));
        uint8_t*  bptr = lineBuffer;
        uint16_t* tptr = (uint16_t*)lineBuffer;
        // Convert 24 to 16 bit colours
        for (col = 0; col < w; col++) {
          b = *bptr++;
          g = *bptr++;
          r = *bptr++;
          *tptr++ = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
        }

        // Push the pixel row to screen, pushImage will crop the line if needed
        // y is decremented as the BMP image is drawn bottom up
        M5.Lcd.pushImage(x, y--, w, 1, (uint16_t*)lineBuffer);
      }
      Serial.print("Loaded in "); Serial.print(millis() - startTime);
      Serial.println(" ms");
    }
    else Serial.println("BMP format not recognized.");
  }
  bmpFS.close();
}

//温度送信する関数
void send_temp(){
  Serial.print("name:");
  Serial.println(id_name);
  Serial.print(" temp:");
  Serial.println(temp);
  Serial.print(" ambient:");
  Serial.println(ambient);  
  Serial.print(" offset:");
  Serial.println(offset); 
   
  WiFiClientSecure sslclient;

  //urlの末尾に測定値を加筆
  url = adress;
  url += key;
  url += "/exec?";
  url += "&1_cell=";
  url += id_name;
  url += "&2_cell=";
  url += temp;
  url += "&3_cell=";
  url += ambient;
  url += "&4_cell=";
  url += offset;  

  // サーバーにアクセス
  Serial.println("サーバーに接続中...");

  //データの送信エラー時
  if (!sslclient.connect(server, 443)) {
    Serial.println("接続に失敗しました");
    Serial.println("");//改行
    return;
  }  
  Serial.println("サーバーに接続しました");

  sslclient.println("GET " + url);
  delay(3000);
  sslclient.stop();

  M5.Lcd.setCursor(5,60);
  M5.Lcd.setTextSize(1);
  M5.Lcd.setTextColor(WHITE, BLACK);    
  M5.Lcd.print("Data uploaded!");

  Serial.println("Send to SpreadSheet");
  Serial.println(url);
  delay(2000);

}

//wifi接続
void connectWiFi(){

  Serial.print("ssid:");
  Serial.print(ssid);
  Serial.println(" に接続します。");

  WiFi.begin(ssid, password);

  Serial.print("WiFiに接続中");
  count=0;
  while(WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
    count++;
    //Serial.println(count);
    if(count>20){//30count以内に接続できない場合はリセット
      esp_restart();
    }
  }

  Serial.println("接続しました。");

  //IPアドレスの表示
  Serial.print("IPアドレス:");
  Serial.println(WiFi.localIP());
}

//基本画面表示
void disp(){
  M5.Lcd.fillRect(1,1,90,60,BLACK);
  M5.Lcd.fillRect(90,1,50,50,BLACK);//x 90+50, y 1+50を消去
  M5.Lcd.setTextSize(1);
  M5.Lcd.setTextColor(WHITE, BLACK);//白文字　黒背景  
  if(id==1){
      M5.Lcd.setCursor(20,10);
      M5.Lcd.print("[name1]");
      id_name="[name1]";
      drawBmpFile(SPIFFS, "/tokage.bmp", 90, 15);//bmpを読み込んで表示する関数
  }
  else if(id==2){   
      M5.Lcd.setCursor(20,10);
      M5.Lcd.print("[name2]");
      id_name="[name2]"; 
      drawBmpFile(SPIFFS, "/neko.bmp", 90, 15);//bmpを読み込んで表示する関数    
  }
  else if(id==3){   
      M5.Lcd.setCursor(20,10);
      M5.Lcd.print("[name3]");
      id_name="[name3]";  
      drawBmpFile(SPIFFS, "/tonkatsu.bmp", 90, 15);//bmpを読み込んで表示する関数    
  }
  else{
      M5.Lcd.setCursor(10,10);
      M5.Lcd.print("Push side");
      M5.Lcd.setCursor(10,20);
      M5.Lcd.print("button");      
      drawBmpFile(SPIFFS, "/tapi.bmp", 90, 15);//bmpを読み込んで表示する関数
      if(wifi_mode==1){
        M5.Lcd.setCursor(10,30);
        M5.Lcd.print("wifi:phone");
      }
      else{
        M5.Lcd.setCursor(10,30);
        M5.Lcd.print("wifi:home");
      }
  }

  disp_bat();//バッテリ電圧表示 
}

//温度計測＆表示
void disp_temp(){
    double sum_body=0.0;
    double sum_amb=0.0;
    
  for(int i=0;i<30;i++){//10回取得して平均化
    sum_body += mlx.readObjectTempC();
    sum_amb += mlx.readAmbientTempC();
    delay(10);  
  }
  temp=sum_body/30.0;//平均化
  ambient=sum_amb/30.0;//平均化
  
  offset=ambient*(-0.159)+6.471;//外気温度補正 
  temp=temp+offset;
  Serial.print(temp);
  Serial.print(" *C ");
  Serial.print(ambient);
  Serial.print(" *C ");
  Serial.println(offset);  
  M5.Lcd.setCursor(5,30);
  M5.Lcd.setTextSize(2);
  M5.Lcd.setTextColor(RED, WHITE);
  M5.Lcd.print(temp);     
  M5.Lcd.print(" C");
}

void send_ready(){
  disp_temp();
  send_flg=0;
  M5.Lcd.setTextSize(1);
  M5.Lcd.setTextColor(WHITE, BLACK);//白文字　黒背景
  M5.Lcd.fillRect(5,60,85,30,BLACK);//5+85, 60+30を塗潰し              
  M5.Lcd.setCursor(5,60);
  M5.Lcd.print("To upload,");
  M5.Lcd.setCursor(5,70);
  M5.Lcd.print("push side");          
  temp_time=0;
  buf_time1=millis();//送信待ちのﾀｲﾏ

  while (temp_time<5000){//10秒間送信待ち                            
    temp_time=millis()-buf_time1;     
    if(digitalRead(buttun_B)==0 && send_flg==0){//flg 0状態でﾎﾞﾀﾝ押されたら1回だけフラグたててBreak
      send_flg==1;
      M5.Lcd.fillRect(5,60,85,30,BLACK);//5+65, 60+30を塗潰し
      Serial.println("送信フラグON");
      M5.Lcd.setCursor(5,60);
      M5.Lcd.print("uploading...");              
      connectWiFi();            
      send_temp();
      WiFi.mode(WIFI_OFF);//完了したらwifi切る
      Serial.println("wifi切断しました。");
      break;
    }
    else if(digitalRead(buttun_A)==0){
      M5.Lcd.setTextColor(WHITE, BLACK);//白文字　黒背景
      M5.Lcd.fillRect(5,60,85,30,BLACK);//5+85, 60+30を塗潰し
      send_ready();       
      break;
    }
  }
  if(send_flg==0){
    M5.Lcd.setTextColor(WHITE, BLACK);//白文字　黒背景
    M5.Lcd.fillRect(5,60,85,30,BLACK);//5+85, 60+30を塗潰し 
  }  
}

void ch_id(){
  id++;
  if(id>3){
    id=1;//idリセット
  }
}

//バッテリ電圧を表示
void disp_bat(){
  V_bat=M5.Axp.GetBatVoltage();
  I_bat=M5.Axp.GetBatCurrent();  
  M5.Lcd.setTextSize(1);
  M5.Lcd.setTextColor(WHITE, BLACK);//白文字　黒背景
  M5.Lcd.fillRect(1,90,60,10,BLACK);//5+85, 60+30を塗潰し              
  M5.Lcd.setCursor(90,1);
  M5.Lcd.print("bat:");
  M5.Lcd.print(V_bat);
  M5.Lcd.print("V");   
}

void setup() {
    //ﾎﾞﾀﾝの初期設定
    curr_A=1;
    last_A=1;
    curr_B=1;
    curr_B=1;

    //wifiモード　ボタン押しっぱ起動でスマホ/家wifiと切替
  long count=millis();
  while(millis()-count<3000){
    if(digitalRead(buttun_B)==0){
      wifi_mode=1;
    }
  }
  if(wifi_mode==0){
    ssid = ssid1; 
    password = pass1;
  }
  else if(wifi_mode==1){
    ssid = ssid2; 
    password = pass2;
  }
    //deepsleep設定
    esp_sleep_enable_ext0_wakeup(GPIO_NUM_37,0);//復帰設定 0で復帰

    //M5初期設定
    M5.begin();
    setCpuFrequencyMhz(80);//wifiは最小でも80MHz必要
    Wire.begin(sda, scl);
    mlx.begin();      
    M5.Axp.ScreenBreath(10);// 7-12で明るさ設定
    M5.Lcd.setRotation(3);// 横向き      
    Serial.begin(9600);
    pinMode(buttun_A, INPUT);
    pinMode(buttun_B, INPUT);

    //bmpファイル置き場のSPIFFSマウント確認
    if(!SPIFFS.begin(true)){
        Serial.println("SPIFFS Mount Failed");
        return;
    }

  //スリープタイマカウントを初期化
  curr_time=millis();
  past_time=curr_time;//スリープ用のﾀｲﾏを初期化
  disp_bat();//バッテリ電圧表示

}


void loop() {

  mlx.readObjectTempC();//暖気用常時稼働
  mlx.readAmbientTempC();//暖気用常時稼働

  curr_A=digitalRead(buttun_A);
  curr_B=digitalRead(buttun_B);
  
  curr_time=millis();//現在時刻更新
  count_timer=curr_time-past_time;

  if(count_timer>sleep_time){	// スリープモード移行（ボタンによる復帰のため復帰タイマー時間0）
  Serial.println("寝ます");
  delay(1000);
  M5.Axp.ScreenBreath(0);//ディスプレイを切る 
  esp_deep_sleep_start();
	delay(1000);
  Serial.print("ここは表示されない");  
  }
  
  else{
    if(curr_B != last_B){//ﾎﾞﾀﾝ押して1回だけ実行する
        if(curr_B==0){//ボタン押している間
          ch_id();
        }
        else{//ﾎﾞﾀﾝ押されてないタイミング
          disp();          
        }
        last_B=curr_B;
        past_time=millis();
    }

    if(curr_A != last_A){//ﾎﾞﾀﾝ押して1回だけ実行する
        if(curr_A==0){//ボタン押している間
            
          }   
        else{//ﾎﾞﾀﾝを離したタイミング
          //timerWrite(timer, 0); //WDTをﾘｾｯﾄ
          send_ready();
        }
        last_A=curr_A;
        past_time=millis();
    }
  }
}
```
</div></details>

# 4. gas
送られてきたデータをGASでSpreadsheetに書き込むようにします。
放射温度計は外部温度に依存するので、実測温度に加えて外部温度も記録しておきます。
「all」のシートにまず一旦全てのデータを集約するようにして、各名前の個別シートを別で起こしてフィルタする形にしました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/d9b1e972-81a5-ed24-50fa-6b0c00de90c8.png" width=50%>

実際にこのあと使っていくのは個別シートです。
「=filter(all!A2:C2,all!B2="[名前1]")」などでallシートから名前でフィルタして持ってくるようにしておきます。
ﾏｯﾁする名前がない日付ところはN/A表示されますが、特に影響無いのでそのままで。
好みでヒストグラムとタイムトレンドを表示しておきます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/1836c879-7a65-231e-a966-85a0e2c8ac20.png" width=75%>

GASのスクリプトは下記です。

<details><summary>ソースコードを表示（折りたたみ）</summary><div>

```php
var id = '[SpreadsheetのIDを入力]';//スプレッドシートの指定

function doGet(e) { 
  console.log("start func_doget(e)")
  console.log("check e:",e);

  // 今日の日付
  var d =new Date();
  var date_t = Utilities.formatDate(d, 'Asia/Tokyo','yyyy/MM/dd');
  console.log("date_t:",date_t);
  var rowData = [];       
  rowData[0] = new Date();   //タイムスタンプ入力
    
  for (var param in e.parameter) {   
    var value = e.parameter[param];  //送信されたデータを取得
    rowData[parseInt(param)] = value; //配列に入れ込み
  } 
  var name = rowData[1];
  console.log("name:",name);
  
  //書き込み
  var sheet = SpreadsheetApp.openById(id).getSheetByName('all'); 
  var newRow = sheet.getLastRow() + 1;  // 次の行に入力する     
  var newRange = sheet.getRange(newRow, 1, 1, rowData.length);
  newRange.setValues([rowData]);   //ここでｼｰﾄに書き込み
  
  var result =  'Ok'
  return ContentService.createTextOutput(param);
}

```
</div></details>

スクリプトをwebアプリとしてデプロイしてIDを取得します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/a47a55bf-2909-3976-1fb7-79992c75492a.png" width=50%>

取得したデプロイIDを前半のM5stickCコードへ転記し書き込みします。
忘れないように注意。

# 5. line連携
Lineと連携して、
・体温入力忘れ時の通知
・Lineからの体温の入力
をLineDeveloperのMessageAPIを使ってやります。

GASで時刻でトリガを設定し。Spreadsheet上を検索し、過去数日間で未入力が無いかチェックし、
未入力がある場合はLineで通知を行います。
で、下記のように日付指定して体温を入力したら、Spreadsheetに数値を書き込むようにします。
入力ミス時用に、予期しない形での入力に対してはエラーを返すようにして、
Spreadsheetを保護しておきます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/724f33b8-842c-c358-ba49-2f00f28a1803.png" width=50%>

<details><summary>ソースコードを表示（折りたたみ）</summary><div>

```php
var id = '[SpreadsheetのID]';//スプレッドシートの指定
const ACCESS_TOKEN = '[Lineのアクセストークン]';
const line_bot="[LinebotのID]"

//実行関数
// Lineからの入力ﾁｪｯｸ
function doPost(e) {
  console.log("start func_doPost(e)")
  console.log("check e:",e);
  // WebHookで受信した応答用Token
  var replyToken = JSON.parse(e.postData.contents).events[0].replyToken;
  // ユーザーのメッセージを取得
  var userMessage = JSON.parse(e.postData.contents).events[0].message.text;

  console.log("userMessage:",userMessage);
  console.log(Object.prototype.toString.call(userMessage));

  if(isFinite(userMessage) || userMessage.match("/")){
    console.log("Sheetに体温入力");
    setData(userMessage);
  }
  else{
    console.log("Lineから想定外の入力");
    push_line("入力エラーです。本日分の場合[xx.x], 日付指定の場合は [*/* xx.x]と入力してね。");
    return;
  }
  return;    
}

//行検索する関数
function findRow(sheet, date) {
  var searchDate = Utilities.formatDate(new Date(date), 'Asia/Tokyo','yyyy-MM-dd'); 
  var values = sheet.getDataRange().getValues();

  for (var i = values.length - 1; i > 0; i--) {
    if(values[i][0]=="#N/A"){
      continue;
    }
    var dataDate = Utilities.formatDate(values[i][0], 'Asia/Tokyo','yyyy-MM-dd');
    if (dataDate.match(searchDate)) {
      console.log("dataDate:",dataDate,"::serchDate:",searchDate);
      return i + 1;
    }
  }
  return false;
}

// Line msgをSheetへ入力
function setData(msg){
  console.log("start func_setData")
  console.log("msg:",msg);
  console.log(Object.prototype.toString.call(msg))
  var d =new Date();
  var date_t = Utilities.formatDate(d, 'Asia/Tokyo','yyyy/MM/dd');
  var sheet = SpreadsheetApp.openById(id).getSheetByName('all');
  var lastRow = sheet.getLastRow();　　//最終行取得
  if(msg.match("/")){
    var T=""
    for(var i=0;i<msg.length;i++){
      if(msg[i].match(" ")){
        var y=Utilities.formatDate(d, 'Asia/Tokyo','yyyy');
        date_t=y+"/"+T;
        var S=""
        for(var j=i;j<msg.length;j++){
          S=S+msg[j];
        }
        msg=S;
        break;
      }
      T=T+msg[i];
    }
    console.log("date_t:",date_t)
    console.log("msg:",msg)
  }

  var cells = sheet.getRange(lastRow+1, 1, 1, 3);
  var data=[date_t,"[name1]",msg.split(/\n/)];
  console.log("data",data);
  // getRange([指定行],[指定列],[行範囲数],[列範囲数])
  cells.setValues([data]);
  return;
}

// https://www.pnkts.net/2018/06/03/line-messaging-api/
//Lineへメッセージを送る関数
function push_line(str){
  var url = "https://api.line.me/v2/bot/message/push";
  var headers = {
    "Content-Type" : "application/json; charset=UTF-8",
    'Authorization': 'Bearer ' + ACCESS_TOKEN,
  };

  var postData = {
    "to" : line_bot,
    "messages" : [
      {
        'type':'text',
        'text':str,
      }
    ]
  };

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  return UrlFetchApp.fetch(url, options);  
}

```

</div></details>

定期実行の関数です。
name1 シート上で当日から7日前までさかのぼって、入力があるかどうかを検索します。
値がなかった日付について、Lineで通知メッセージをするようにします。
7日間分ちゃんと体温が入力されてる場合は、OK的なメッセージを返します。


<details><summary>ソースコードを表示（折りたたみ）</summary><div>

```php
// GAS定期実行
// https://qiita.com/rf_p/items/267a8d9daa8c9f1ef027

function TimeTriger(){
  console.log("start func_TimeTriger()")
  var d =new Date();
  var date_t = Utilities.formatDate(d, 'Asia/Tokyo','yyyy/MM/dd');
  console.log("date_t:",date_t);
  console.log("入力状況の検索を開始");
  var sheet = SpreadsheetApp.openById(id).getSheetByName('[name1]');
  // getRange([指定行],[指定列],[行範囲数],[列範囲数])

  var flg=true;
  var date_emp=[];
  // １週間前までで未入力を探す
  for(let i = 7; i >= 0; i--){
    var d_pre =new Date();
    var day=d.getDate();
    d_pre.setDate(day-i);    
    var date_pre = Utilities.formatDate(d_pre, 'Asia/Tokyo','yyyy/MM/dd');
    var row_find = findRow(sheet, date_pre);
    console.log("date_pre:",date_pre,":: row_find:",row_find)
    if(row_find==false){
      date_emp.push(date_pre);
      flg=false;
    }
  }
  console.log("date_emp:",date_emp);

  // 未入力日付をlineにPush

  if(flg==false){
    var blank_l=date_emp.length
    var txt="";
    for(var i=0;i<blank_l;i++){
      txt=txt+date_emp[i]+"\n"
    }
    txt=txt+"上記体温入力がありません。";
    console.log(txt);
    push_line(txt);
    return;
  }
  else{
    // その日の体温を返す
    // var row_find=findRow(sheet, date_t);
    // var val=sheet.getRange(row_find, 3, 1, 1).getValues();
    // console.log("val:",val);
    // var txt= date_t+"の体温は"+val+"℃です。"
    var txt="体温入力OK"
    push_line(txt)
  }  
}

```

</div></details>
トリガの設定部分です。
時間ベースでの実行にしています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/1777142a-6395-3ada-e858-f21ea0062e05.png" width=50%>

ついでにLineのMessageAPIの画面もスクショします。
Webhoook URLにデプロイしたGASのIDをコピペしておきます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/8543c718-2c1a-1992-9d03-16c71d41b7dd.png" width=50%>

# 6. excel連携
これでSpreadsheet上にﾃﾞｰﾀをいい感じに保存できる環境ができたので、あとはｴｸｾﾙからﾃﾞｰﾀを取りに行きます。
ﾊﾟﾜｰｸｴﾘでwebｱｸｾｽして、持ってくるだけです。
下記を参考にさせてもらいました。

https://trendlife.co.jp/excel-powerquery003/

これで所望のSheet, セルのﾃﾞｰﾀを下記のように引っ張ってこれます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/3773d13e-fb85-310f-ceac-69c5c7edf8f3.png" width=50%>

今回はこれを定期的に更新したいです。
なのでまず「ﾌﾞｯｸの接続」からｴｸｾﾙを起動したら自動で更新されるようにします。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/a5aaed66-9b61-1cb4-9637-c381ab57e3a0.png" width=50%>

で、windowsﾀｽｸｽｹｼﾞｭｰﾗでこのｴｸｾﾙを毎日12:30に実行するようにします。
毎度ｴｸｾﾙが立ち上がるので、上書き保存が必要ですが入力確認も兼ねるということでそのままにしておきます。
自動で閉じるのであれば、ｴｸｾﾙ側にﾏｸﾛでも仕込んでおけばいいのかな。

あとは入力シートに合わせて、日付などをキーにして体温を持っていきます。
lookupなり、index&matchなりなんなりで、うまいことやります。
会社のシートを晒すのもちょっとアレなので、この辺までにしておきます。

# 7. おわりに
一気に書いたらだいぶ長くなってしまいました。。。

やりたいことはとりあえず実装できました。
LineBotとか無料範囲でも結構やれるんだなーと気づけました。
ﾒｯｾｰｼﾞを遅れるのは何かと便利な気がするので、他のｻｰﾋﾞｽとかﾃﾞｰﾀと連携して、
色々賢いBotに育ててあげれたらいいな。