【GAS】IFTTT無しで自室の照明操作（Webhook→GAS→Nature Remo Cloud API）

# はじめに

2024年2月15日より、IFTTTでのWebhook実行が有料化されたみたいです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3175876/7d678462-2889-772e-eff5-d85ee21bb6e4.png)

我が家はNature Remo nanoを導入しており、自室の照明は[Nature Remo Cloud API](https://developer.nature.global)経由で操作できます。
僕は睡眠解析アプリ（[Sleep as Android](https://play.google.com/store/apps/details?id=com.urbandroid.sleep)ってやつです）で「就寝時に部屋の照明を常夜灯に」「起床時に部屋の照明をONに」するようにしていたのですが、
これは **睡眠解析アプリ→IFTTT→Webhook→Nature Remo→家電操作** で実現していたため、IFTTTでのWebhookが使用できなくなると動かなくなります。

このままでは朝起きられなくなってしまうので、IFTTTを使わず上記フローを実現してみようと思います。


# 作ってみる

本記事のゴールは上記フローのIFTTT部分をGASで置き換え、
**睡眠解析アプリ→GAS内でWebhookへPOST→Nature Remo→家電操作** の流れを実現することとします。


## Webhookの連携内容を確認(睡眠解析アプリ→Webhook)

Sleep as AndroidのWebhookでのPOST内容は[ここのページ](https://docs.sleep.urbandroid.org/services/custom_webhooks.html)で確認できます。
見る限り`event: event_name`からイベント内容を取得できそうなので、
「`sleep_tracking_started`で照明を常夜灯」「`sleep_tracking_stopped`で照明をON」とするように組んでみます。

ぶっちゃけトリガーは睡眠解析アプリである必要も無いので、やりたいことに応じてフロント側を実装するで良いと思います。


## IFTTTの代わりにGASでWebhookの受け口を作る(GAS→Nature API)

無料で簡易なWebアプリを作れるでおなじみ、Google Apps Scriptにお世話になることとします。


### Nature APIのアクセストークンを取得

Nature APIの操作にはアクセストークンが必要なので、事前に取得しておきます。
[Access Tokens](https://home.nature.global/home) ページから `Generate Access Token` でアクセストークンを取得します。
一度ページを離れると再表示できないので、確実にメモっておきます。

:::note warn
このトークンがあれば家電操作できてしまうため、第三者には絶対見られないようにしましょう。
もし見られてしまった場合、上記ページから `Client name : me` となっている行の `Revoke` を押してトークンを無効化してから再発行しましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3175876/848625af-5e73-19bc-8a91-d37eef45ee3d.png)
:::

GASでプロジェクトを作成し、 **プロジェクトの設定 > スクリプト プロパティ** から先ほどのトークンをプロパティに入力しておきます。
（ `APPLIANCE_ID` は後ほど入れます）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3175876/82873acb-8f73-90a9-0087-68fe45f6aa6d.png)

ソースコードに直書きしても動きはしますが、今回はトークンの流出防止としてこのような作りにしてみます。


### Appliance ID(家電1つに紐づくID)を取得

Nature APIは[ここのページ](https://swagger.nature.global)で内容確認できます。
今回は照明操作なので `1/appliances/{applianceid}/light` へPOSTすることになりますが、
`applianceid`(家電1つに紐づくID)と`button`(操作するボタンの種類)が必要なのでまずはそれを取得します。

```js
/**
 * 家電操作用のAPPLIANCE_IDを取得
 */
function getApplianceId () {
  const appliances = getNatureApi_('/1/appliances').filter((appliance) => appliance.type === 'LIGHT');
  Logger.log(appliances.map(appliance => ({
    nickname: appliance.nickname,
    id: appliance.id,
    buttons: appliance.light.buttons})
    ));

}

/**
 * Nature API(GET)を実行
 * @params apiPath : APIのパス(https://swagger.nature.global/#/)
 */
const getNatureApi_ = (apiPath) => {
  const options = {
    headers: {'Authorization': `Bearer ${PropertiesService.getScriptProperties().getProperty('NATURE_API_ACCESS_TOKEN')}`},
  };
  return JSON.parse(UrlFetchApp.fetch(`https://api.nature.global${apiPath}`, options).getContentText());
};
```

`getApplianceId()`を実行するとGASのログにJSONとして出力されます。
ここから`applianceid`・`button`をメモっておき、
`applianceid`はアクセストークンと同様にスクリプトプロパティに登録しておきます。

:::note
`getNatureApi_()`の関数名末尾にアンダースコアがついていますが、これをつけることでprivate関数扱いになるみたいです。
サーバー側の関数呼び出しの対象外になったり、GASのエディター上からも直接実行できないようにできるので
外部から呼び出す想定のある関数以外はアンダースコアをつけたほうがいいのかもしれないです。
:::


### POSTの受け口を作る

`doPost`関数を用意することでWebhookを受けられるようになる、みたいです。
睡眠解析アプリからのPOST内容をもとに「どのイベントがあったかを判別→リモコンの信号を送信」といったソースを組んでみます。

```js
/**
 * Webhookトリガーで照明を操作
 */

// Sleep側のイベントと押すボタンの種類の対応
const watchActions = [
  {event: 'sleep_tracking_started', button: 'night'},
  {event: 'sleep_tracking_stopped', button: 'on'},
];


/**
 * POSTの受け口
 */
const doPost = (e) => {
  const payload = JSON.parse(e.postData.getDataAsString());
  Logger.log(payload);

  // 行動が照明操作対象であれば、ボタンを操作する
  const action = watchActions.filter(action => action.event === payload.event);
  if (action.length) {
    postNatureApi_(`/1/appliances/${PropertiesService.getScriptProperties().getProperty('APPLIANCE_ID')}/light`, {button: action[0].button});
  }
};

/**
 * Nature API(POST)を実行
 * @params apiPath : APIのパス(https://swagger.nature.global/#/)
 */
const postNatureApi_ = (apiPath, args) => {
  const options = {
    method: 'post',
    headers: {'Authorization': `Bearer ${PropertiesService.getScriptProperties().getProperty('NATURE_API_ACCESS_TOKEN')}`},
    payload: args,
    };
  UrlFetchApp.fetch(`https://api.nature.global${apiPath}`, options);
};
```

`watchActions`でどのイベントを拾ってどんな信号を送信するかを定義しています。
本当はこの定義はスプレッドシートでやりたかったのですが、GASからのスプレッドシート値取得ってたまに失敗するんですよね……。
確実に動作させたかったので今回はソースに直書きしています。

:::note
デバッグとして、`e.postData.getDataAsString()`でPOSTされたRequest Bodyを取り出した段階でログ出力しています。
ですが`doPost()`の内容はこのままではログで確認できません。
以下ページを参考に、Google Cloud Platform(GCP)でログを出力できるようにしてみました。ここが一番大変だった。
[Google Apps ScriptでdoPost関数のログを出力する方法3選 | prtn-blog](https://prtn-life.com/blog/gas-log-gcp#toc6)
:::


### Webアプリとして発行

GASのページ右上の `デプロイ > 新しいデプロイ` から、作成したコードをWebアプリとして発行できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3175876/e29a4313-d7af-1f27-b790-e7ef53c5ca37.png)

`アクセスできるユーザー : 全員`にした状態でデプロイし、ウェブアプリのURLをメモっておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3175876/472da308-b483-f5da-31ba-fb9ef2686b3a.png)

あとはこのURLにWebhookをPOSTすれば動くはずです。
もしうまく動かない場合はログを確認し、ソースを適切に修正した上で再デプロイしましょう。
その際は`デプロイを管理 > 右上の編集ボタン`を押下し、`バージョン : 新バージョン`を選んで再デプロイします。


# おわりに

デバッグが一番苦労しましたが、無事IFTTT無しで以前と同様のことが実現できて良かったです。
タイムラグとしても1~2秒ほどで、感覚としては「睡眠解析を始めたらすぐ暗くなる」感じ。

ソースとしてはかなりシンプルなので、やりたいことに応じて適宜改変すれば色々できそうですね。
温度センサーと組み合わせて空調を操作させたり、フロント部分をSlackやDiscordなんかに変えても良いですね。何ならWebhookさえ投げられれば物理ボタンでも良いし。


# 参考にさせて頂いたHP
- [Nature Remo + Google Apps Scripts（GAS） + Slack App を使ってサーバ管理不要でプログラム可能なスマートホームを実現する 後編 - s平面の左側](https://blog.okashoi.net/entry/2022/12/06/000000)
→家電の種類・フロント部分は違いますが、ソースとしては一番参考にさせて頂きました。
POSTのイベント取得などがこのままでは動かなかったため、改変はしています。

- [GASのScriptPropertiesクラスがサポート終了！スクリプトプロパティの取得方法は？ | AutoWorker〜Google Apps Script(GAS)とSikuliで始める業務改善入門](https://auto-worker.com/blog/?p=7793)
→スクリプトプロパティの取得部分が、上の記事の関数は現在非推奨みたいので代替。

- [Google Apps ScriptでdoPost関数のログを出力する方法3選 | prtn-blog](https://prtn-life.com/blog/gas-log-gcp#toc6)
→前述しましたが、`doPost()`からログを出すためのGCPへの紐づきはこちらを参考にさせて頂きました。

- [Google Apps Scriptでプライベート関数を宣言し、「実行する関数を選択」に表示させない - 毎日へっぽこ](https://hepokon365.hatenablog.com/entry/2022/04/29/231744)