テレビ局員の独学プログラム

ともきちの日曜プログラムとガジェット

最近プログラムにはまっているので、はてなブログもやってみる。プログラムは完全独学なのでいろいろご愛嬌。

GASからNatureRemoのAPIを叩く!

目覚ましと同時に電気をつけたい

私を朝が弱い&遮光カーテンを使っているせいで、目覚めがとても悪い。
そこで、目覚ましと同時に電気をつければ、少しは体が反応して目覚めがよくなるのではと思った。

Nature Remoとは

Nature Remoはスマートリモコンの1つで、あらゆる家電の赤外線リモコンの代わりを担ってくれる。
私はNatureRemoかなり昔から使っているが、今までは主にアレクサ経由での操作しかしていなかった。

アレクサ、電気つけてー
アレクサ、テレビつけてー

Nature Remoの自動化

Nature Remoアプリ自体にスケジュール機能のようなものは、IFTTTを使用すれば簡単なスケジューリングならすることが可能である。
ただし、IFTTTの「Date&Time」は決まった時間に実行することができない。

私は規則正しい生活をしていないので、毎日決まった時間に電気をつけられても困る。
寝る前に「明日は○時に起きる」と決めたいのだ。

そこで、NatureRemoにAPIがあるので、これをいい感じに叩けば、目覚ましと同時に電気をつけることができるのではないか!?

GASから Nature RemoのAPIを叩いてみる

前置きが長くなったが、ようやく本題に入る。
今回も素人コードなので、ご容赦ください。もっとこうした方がいいよ等あればアドバイスください。

トークン取得

まずはNature RemoのAPIを叩いてみるのだが、Nature Remoのトークン取得やテストはこちらの記事がわかりやすいので、おまかせする。
APIドキュメントはこちら

Signal IDの一覧取得

https://api.nature.global/1/appliancesへgetすると、現在自分が登録しているアクションに対応するSignal IDの一覧が取得できる。

 [
	{
	"id":"08ba7104-0486-4008-9a9a-0d02a0547d66",
	"device":{
		"name":"Remo1go","id":"ccc22cfc-fd1e-4acf-bb89-94279778345a",
		"created_at":"2018-07-25T16:12:58Z",
		"updated_at":"2020-06-09T11:16:26Z",
		"mac_address":"dc:4f:22:3e:da:10",
		"serial_number":"2W8602345",
		"firmware_version":"Remo-mini/1.0.87-g8b06f0e",
		"temperature_offset":0,
		"humidity_offset":0
		},
	"model":null,
	"type":"IR",
	"nickname":"電気",
	"image":"ico_light",
	"settings":null,
	"aircon":null,
	"signals":[
		{
		"id":"6a8ea820-fc34-461c-a27e-0c7989584595",
		"name":"点灯","image":"ico_on"
		},
		{
		"id":"1608f92c-2fcc-4e0d-a295-17f6083555c0",
		"name":"消灯",
		"image":"ico_off"
		},
		{
		"id":"e99653cb-1e35-46ab-a6f0-cfde33b92e5a",
		"name":"常夜灯",
		"image":"ico_record"
		}
	      ]
	}
]


取得した結果の必要そうな情報はスプレッドシートへ書き込んでおく。
アクセストークンとスプレッドシートのIDはスクリプトのプロパティで設定している。

// 最新のsignalidを取得
function getSignalIdList() {
  
  var token ="Bearer " + PropertiesService.getScriptProperties().getProperty('REMO_ACCESS_TOKEN');  
  var sheetid = PropertiesService.getScriptProperties().getProperty('SHEET_ID_SIGNALS');  
  
  // 機器情報を取得
  var options = {
    'method': 'get',
    'headers': {
       'Authorization': token
    }
  };
  var response = UrlFetchApp.fetch('https://api.nature.global/1/appliances', options);
  Logger.log(response)
  r = JSON.parse(response.getContentText())
  //Logger.log(r)
  
  // 書込み用スプレッドシートを開いて、全クリア
  var spreadsheet = SpreadsheetApp.openById(sheetid);
  var sheet = spreadsheet.getActiveSheet();
  sheet.clear()
  
  var today = new Date();
  sheet.getRange(1, 1).setValue("更新日時");
  sheet.getRange(1, 2).setValue(today);
  
  
  // 取得したデータを解析、スプレッドシートに書き込み
  var rownum = 2
  for (let i=0; i<r.length; i++) {
    var name = r[i]["nickname"]
    if (r[i]["signals"].length !== 0) {
      for (let j=0; j<r[i]["signals"].length; j++) {
        var signal_name = r[i]["signals"][j]["name"]
        var signal_id   = r[i]["signals"][j]["id"]
        
        sheet.getRange(rownum, 1).setValue(name);
        sheet.getRange(rownum, 2).setValue(signal_name);
        sheet.getRange(rownum, 3).setValue(signal_id);
        Logger.log(name+signal_name+signal_id)
        rownum = rownum + 1
      }
    }
  }
}

実行するとスプレッドシートはこんな感じ。
f:id:mtomo661:20200610001413p:plain

照明を操作してみる

signal_idを引数と渡して、そのsignal_idをRemoへ送る関数を作っておく。

// Remoにsignalidを送信
function doRemo(signal_id) {

  var token ="Bearer " + PropertiesService.getScriptProperties().getProperty('REMO_ACCESS_TOKEN');      
  var options = {
    'method': 'post',
    'headers': {
       'Authorization': token
    }
  };
  
  var url = "https://api.nature.global/1/signals/" + signal_id + "/send"
  var response = UrlFetchApp.fetch(url, options);
  return response;
}

照明を消すsignal_idを送ってみる。

function test() {
 var signal_id = "1608f92c-2fcc-4e0d-a295-17f6083555c0" 
    result = doRemo(signal_id) 

おー、消えた!簡単だ。

GASのトリガーを使って、指定の時間に実行

あとはトリガーセットするだけ!と思ったが、GASのトリガーはいろいろとクセがあったので、その話は次回。

素人がherokuを使用するまで

pythonを定期実行したくて、調べるとherokuというのが出てくる。
素人にはハードル高かったので、忘備録。

gitをインストール

herokuを使うにはgitをインストールする必要がありそう。
gitの使い方は以下を参考にさせていただきました。
非常にわかりやすかった。
ただ、configを使ったssh認証はうまくいかなかったので断念。

今日からはじめるGitHub 〜 初心者がGitをインストールして、プルリクできるようになるまでを解説 - エンジニアHub|若手Webエンジニアのキャリアを考える!

herokuを登録・CLIインストール

herokuを使うには、アカウント登録とherokuをコマンドで実行するためのCLIのインストールが必要。
このあたりは参考記事がたくさんあるので、よきものを参考。

使用するpythonファイル等を準備

今回は以下のフォルダを準備

test_heroku
 ┣ slack_notice.py
 ┣ requirements.txt
 ┗ runtime.txt

requirements.txt には必要なパッケージを書く。
ローカルと同じ環境で動かすのであれば、pip freeze > requirements.txt で出力すればOK。
runtime.txt には実行するpythonのバージョンを書く。
今回はpython-3.7.3を使用。使用できるバージョンは公式を参照。

#runtime.txt
python-3.7.3

herokuへアップロード

準備ができたら、herokuにアップロードしていく。

$ heroku login
$ heroku create

heroku login を実行すると、ブラウザに移行してログインする
heroku createでherokuが自動でつけた名前のアプリがheroku上に作成されるのでブラウザで確認する

f:id:mtomo661:20190414222602p:plain

上記の場合は3つのアプリが作成されている。

作成されたアプリを選択、「deploy」を見るとアップロードする手順が書いてある。

f:id:mtomo661:20190414223122p:plain

基本的には手順に従えはOK。
所々、上記のサイトのgit手順に従う。

$ cd test_heroku
$ git init
$ heroku git:remote -a xxxxxxxxxxx
$ git add .
$ git commit -a
$ git push heroku master

git push heroku master を実行すると、パッケージのインストールなどが開始される

c:\Users\xxxx\git\test_github>git push heroku master
Enumerating objects: 12, done.
Counting objects: 100% (12/12), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (12/12), 3.23 KiB | 300.00 KiB/s, done.
Total 12 (delta 1), reused 3 (delta 0)
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Python app detected
remote: -----> Installing python-3.7.3
remote: -----> Installing pip
remote: -----> Installing SQLite3
remote: -----> Installing requirements with pip
remote:        Collecting altgraph==0.16.1 (from -r /tmp/build_87a75a556e8eb4dd0ab66965df5e0b56/requirements.txt (line 1))
remote:          Downloading https://files.pythonhosted.org/packages/0a/cc/646187eac4b797069e2e6b736f14cdef85dbe405c9bfc7803ef36e4f62ef/altgraph-0.16.1-py2.py3-none-any.whl
・
・
・
remote: Verifying deploy... done.
To https://git.heroku.com/xxxxxxxxxxx
 * [new branch]      master -> master

これでherokuへのアップロードが完了。

pythonを実行

あとは、下記コマンドでpythonが実行できる

$ heroku run slack_notice.py

ちなみに、slack_notice.pyは中身は下記である。

import requests
import json

slack_post_url = "slackアクセストークン"
name = "python_taro"
text = "testyade"

requests.post(
    slack_post_url,
    data=json.dumps(
        {"text": text,
         "username": name,
         "icon_emoji": ":python:"}))


f:id:mtomo661:20190414224059p:plain

見事にslackに通知が来た。
slackにはpythonの絵文字を登録している。

あとは定期実行すればよいが、定期実行にはアドオンの使用、アドオンの使用にはクレジットカードの登録が必要なので、とりあえず、今回はここまで。

Gmailの添付ファイルをDropboxに保存する

前回、Gmailの添付ファイルをgoogle driveに保存したが、今回はなぜかDropboxに保存する。
やろうとする人がいないと思うので、GASからDropbox APIを操作する記事が見当たらなかったので苦労した。

Dropbox APIのアプリを作成する

Dropboxに外部からアクセスするには、 Dropbox APIのアプリを作成する必要がある。
Dropbox APIのアプリを作成するには、Dropbox developersに登録する必要があるがそれは割愛

  1. https://www.dropbox.com/developersへアクセスして、「app console」をクリック
  2. アカウントを持っている場合はログイン
  3. My appsが表示されるので、「Create app」をクリック
  4. 今回は「Dropbox API」「App folder」を選択(既存フォルダにアクセスした場合は「Full Dropbox」で作成する必要あり)名前をつけて「Create app」をクリック
  5. Generated access tokenの「Generate」をクリック

これでとりあえず、外部からアクセス可能

コマンドを確認する

https://dropbox.github.io/dropbox-api-v2-explorer/APIの動きやコマンドを確認することができる

リストを取得する

まずは試しに、dropboxないのリストを取得してみる

  1. 左のリストから「list_folder」を探してクリック
  2. 先ほど取得したAccess Tokenを「Access Token」に貼り付けて、「Submit Call」をクリック。ResponseがJSON形式で帰ってくる。list_folderと言いつつ、ファイルも返ってくる。
  3. フォルダを指定した場合は「path」に「/testfoler」と指定する
  4. サブフォルダまで探す場合は「list_folder/continue」を使うみたいだが、今回はスルー
  5. 「Show Code」をクリックすると、サンプルコマンドが表示される。

HTTP POSTする際のheaderやparameterの書き方の参考にするため、よく見ておく

ファイルをアップロードする
  1. 次は左のリストから「upload」を探してクリック
  2. 今度は先ほどと同じAccess Tokenがすでに入っているので、Access Tokenの入力はなし
  3. 「File to upload」でアップしたファイルを選択。「path」に「/up_testfile.txt」と入力(アップロードは必ずファイル名を入力する必要がある。めんどくさい)
  4. 「mode」は「overwrite」を選択。(未選択で同名のファイルがある場合はエラーになる。「add」は別名保存、「update」は既存ファイルをリネームっぽい)
  5. 「Submit Call」をクリックすると、ResponseがJSON形式で帰ってくる。
  6. こちらも「Show Code」して、中身をよく見る。ふむふむ

list_folderとはいくつか違うので注意が必要。最初気付かず、しばらくはまった。。

  • アクセスURLが違う
  • headerにDropbox-API-Argが必要。Dropbox-API-ArgはJSON形式に注意。
  • parameterはファイルをそのまま書くだけ

GASからHTTPリクエストを送る

動きがなんとなく確認できたら、GASからリクエストを送っていく。GASでHTTPリクエストする際はUrlFetchApp.fetchを使用。

リストを取得する
function get_dbxList(Appkey,path) {
/*
dropboxのフォルダ・ファイルリストを取得する
rootを指定する場合はpathに””を指定
*/

  return UrlFetchApp.fetch(
    "https://api.dropboxapi.com/2/files/list_folder",
    {
      "method" : "post",
      "headers" : {
        "Authorization" :"Bearer " + Appkey,
        "Content-Type" : "application/json"
      },
      'payload' : JSON.stringify({
        "path" : path
      }),
      "muteHttpExceptions" : false
    }
  );
}

ファイルをアップロードする
function upload_dbx(Appkey,src,dest) 
{
/*
dropboxにファイルをアップロードする
rootにアップする場合もファイル名を必ず指定する必要あり
*/

  return UrlFetchApp.fetch(
    "https://content.dropboxapi.com/2/files/upload",
    {
      "method" : "post",
      "headers" : {
        "Authorization" :"Bearer " + Appkey,
        "Content-Type" : "application/octet-stream",
        "Dropbox-API-Arg" : "{\"path\":\"" + dest + "\",\"mode\":{\".tag\":\"overwrite\"}}"
      },
      // Convert the JavaScript object to a JSON string.
      'payload' : src ,
      "muteHttpExceptions" : false
    }
  );
}

Gmailの添付ファイルをDropboxに保存する

ここまでくれば、前回の記事と組み合わせるだけ。


mtomo661.hatenablog.jp


dropboxは保存するファイル名を必ず指定する必要があるので
取得した添付ファイルのファイル名を取得して、アップロードする際に使う。

function Attachment_to_dbx() {
  
Appkey = ”dropboxのアクセストークン”  
 
  //昨日の日付計算(検索条件用)
  var now = new Date();
  var yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);  
  yesterday = Utilities.formatDate(yesterday, "JST", "yyyy/MM/dd");
    
  //検索条件設定(Gmail で使用できる検索演算子 参照)
  var condition;
  condition  = " is:unread"  //未読
  condition += " has:attachment";  //添付あり
  condition += " after:" + yesterday;  //今日(昨日より後)
//  condition += " subject:添付やで";  //件名
//  condition += " from:hogehoge@じーめーる"
    
  //条件でメール検索
  var search_mail = GmailApp.search(condition);
  //検索したメールをスレッドで取得(二次配列)
  var messeges = GmailApp.getMessagesForThreads(search_mail);

  //スレッド数ループ
  for(var i = 0; i < messeges.length; i++) {
    //スレッドの中身数ループ
    for(var j = 0; j < messeges[i].length; j++) {
      //添付ファイル取得(一次配列)
      var attach = messeges[i][j].getAttachments();
      //添付ファイル数ループ
      for(var k = 0; k < attach.length; k++){
       //添付ファイルのファイル名取得
        fileName = attach[k].getName()
        Logger.log(fileName)
        //添付ファイル保存
        src = attach[k]
        dest = "/" + fileName
        upload_dbx(Appkey, src, dest) 
      }
    }
    //スレッドが終わったら、既読に
    messeges[i][0].markRead();
  }
}

あとは、前回同様、定期実行設定すれば自動保存されていく。
以上、なぜかGmailの添付ファイルがgoogledriveではなくDropboxに保存する方法。

 

Gmailの添付ファイルをgoogle driveに保存する

IFTTTのGmail連携が終了してしまった。
Gmailの添付ファイルを自動でgoogle driveに保存するプログラムをGASで作成して
定期実行すれば代わりになりそう。

GASは参考になるものがたくさんあるので、割と簡単。エラー処理はなし。
以下の点を工夫した。

  • 本日分の未読メールだけ検索。
  • 処理が終わったら既読にすることで、次の実行時は対象外。

検索条件を指定する

検索条件は後から変えやすいように記述。検索条件設定の先頭にスペースつけるの注意。
条件の書き方はGmail で使用できる検索演算子 を参照)

//昨日の日付計算
  var now = new Date();
  var yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);  
  yesterday = Utilities.formatDate(yesterday, "JST", "yyyy/MM/dd");

  var condition;
  condition  = " is:unread"  //未読
  condition += " has:attachment";  //添付あり
  condition += " after:" + yesterday;  //今日(昨日より後)
//  condition += " subject:添付やで";  //件名
//  condition += " from:hogehoge@めーる" //差出人

メールを検索する

上記の条件でメールを検索する。
GmailApp.getMessagesForThreads()は二次配列で返ってくるので注意。
1つのスレッドが返信を含めた複数のメールの一次配列となる。

  var search_mail = GmailApp.search(condition);
  var messeges = GmailApp.getMessagesForThreads(search_mail); //これが二次配列

ループ処理しつつ、添付ファイルを保存する

今回はgoogle driveに保存するので、保存するフォルダのIDを調べる。
フォルダIDはフォルダURLの~folders/以降となる。
処理が終わったスレッドは既読にする。

 var hozon_folder = DriveApp.getFolderById('フォルダID');

//スレッド数ループ
 for(var i = 0; i < messeges.length; i++) {
    for(var j = 0; j < messeges[i].length; j++) {
      var attach = messeges[i][j].getAttachments();
      for(var k = 0; k < attach.length; k++){
        hozon_folder.createFile(attach[k]);
      }
    }
    messeges[i][0].markRead();
  }

定期実行

これを、定期実行すれば自動保存されていく。
GASの定期実行はプロジェクトのトリガーを設定するだけ。
f:id:mtomo661:20190414015732p:plain

5分に1回なのか、1時間に1回なのかはお好みで。
無料ユーザーは定期実行の制限に注意。(これくらいは大丈夫)

まとめ

function Attachment_to_GDrive() {
  
  /*
  条件に合うgmailの添付ファイルをgoogledriveに保存する
 検索条件は先頭にスペースつけるの注意
  */
  
  //昨日の日付計算(検索条件用)
  var now = new Date();
  var yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);  
  yesterday = Utilities.formatDate(yesterday, "JST", "yyyy/MM/dd");
  
  //保存先フォルダ設定(フォルダURLの~folders/以降) 
  var hozon_folder = DriveApp.getFolderById('google drive フォルダID');
  
  //検索条件設定 (Gmail で使用できる検索演算子 参照)
  var condition;
  condition  = " is:unread"  //未読
  condition += " has:attachment";  //添付あり
  condition += " after:" + yesterday;  //今日(昨日より後)
//  condition += " subject:添付やで";  //件名
//  condition += " from:hogehoge@じーめーる"
    
  //条件でメール検索
  var search_mail = GmailApp.search(condition);
  //検索したメールをスレッドで取得(二次配列)
  var messeges = GmailApp.getMessagesForThreads(search_mail);

  //スレッド数ループ
  for(var i = 0; i < messeges.length; i++) {
    //スレッドの中身数ループ
    for(var j = 0; j < messeges[i].length; j++) {
      //添付ファイル取得(一次配列)
      var attach = messeges[i][j].getAttachments();
      //添付ファイル数ループ
      for(var k = 0; k < attach.length; k++){
        //添付ファイル保存
        hozon_folder.createFile(attach[k]);
      }
    }
    //スレッドが終わったら、既読に
    messeges[i][0].markRead();
  }
}

今回は保存用に転送したメールの添付ファイルを保存する想定なので未読メールだけを処理。
既読のメールも処理する際は、定期実行の前回実行時間以降を処理(5分に1回なら、5分前以降)するなどの工夫が必要。
ただし、GASの定期実行は負荷分散を考慮して正確ではないので、タイミングによっては逃したり、重複保存されるかも。