SwitchBotの電球の色を卸電力市場価格連動で変化させる

# はじめに

「市場連動」の電気料金メニューを知っていますか？　電気料金単価が、30分ごとに「卸電力市場」の価格に連動して変化するという電気料金のメニューで、一部の電力会社が提供しています。電力の需要と供給のバランスで決まるという卸電力市場価格に連動しているため、例えば太陽光発電からの供給が多い春秋の昼間には、安い電気料金単価になります。当然安くなるだけでなく、電力の需要が多いときには、電気料金単価が大幅に上がってしまうこともあるので注意が必要です。

では、今、電気を使うとその単価はいくらなのか。電力会社提供のアプリなどで料金単価を確認することはできますが、いちいち開くのは手間です。タブレットなどで常に表示しておくことも考えられますが、直感的にはわかりにくい。

そこでタイトルにあるように、ここではSwitchBot社の「スマート電球」の色を変化させることで、今の卸市場価格を表示してみることにしました。
なお「卸市場価格連動」というところを変更すれば、時刻に連動させる、（データさえ取ってこれれば）今の気温に連動させる、株価に連動させるなども可能ですので、SwitchBot社のスマート家電のコントロール自体に関心をお持ちの方にもご参考になればと思います。

## スマート電球の色を変化させるには？

どうやるのか、のイメージから（必ずしも正確ではありませんが、全体感を理解する助けになれば幸いです）。

スマート電球を購入しWiFi設定を行うことで、電球がメーカーのサーバーと通信できるようになっています。専用アプリから電球のオンオフや色設定を指示すると、アプリからサーバー、サーバーから電球に指令が送られて、オンオフ状態や色が変わります。専用アプリ上で用意された条件の形式（例えば時刻や照度センサーとの連動など）であれば、自動操作も行うことができます。これが普通のスマート電球の使い方です。

ここで、電球の色を、専用アプリでは想定されていない特定のデータに自動で連動させるために、APIを利用します。
一般にAPIとは、あるシステムとの情報のやり取りを、部外者が簡単な手順で行えるように用意・公開された仕組みです。API利用は有料の場合もあれば無料の場合もあります。
APIはHTTPプロトコルを利用している、すなわち、情報のやり取りのためのリクエストは「http://」から始まるURLのような形で表現されます。とても単純なリクエストであれば、ブラウザのURL入力欄に入力して送り、そのブラウザ上で結果を見ることもできます。

言い換えると、APIを利用するとは、何らかのプログラミング言語で、リクエストを表す変数を生成して、その言語に用意されているURLリクエスト用の関数を作って送ることです。リクエストをどのような書式で生成すればよいかは、そのAPIの仕様書に示されています。

スマート電球では、オンオフや色設定に対応するリクエストをメーカーのサーバーに送ると、専用アプリからの操作と同じようにサーバーから電球に指令が送られて、オンオフ状態や色が変わります。ですので、特定のデータに基づき自分のスマート電球の色を指定するというリクエストを生成し、それを送るというプログラムを記述します。そして、このプログラムを、定期実行させればよいのです。

## 利用環境
以下のものを準備しました。
#### SwitchBot社のスマート電球とアプリ
電球1つであれば2000円くらいで売られているようです（ただ当然、電球をセットできる照明器具が必要です）。購入したら照明器具にセットし、専用アプリから初期設定をして、アプリから操作できることを確認しておきます。
SwitchBot社のスマート製品のAPIは無料公開されています。
#### プログラミング環境
ここでは、Google Apps Script (GAS)としました。Googleのアカウントを持っていたら無料で使えること、またその定期実行の指定が簡単にできることが理由です。GASは、Google版のマクロ(VBA)のようなもので、JavaScript系の言語だそうです。ユーザインタフェースが必要な場合にはエクセルマクロのようにスプレッドシートに紐づけると便利ですし、そうでない場合は単独で作ることもできます。
以降の説明中のコードは、特に省略していませんので、全文コピペすると（ご自身のSwitchBotに関する情報を入力する必要があります）動くようになっているはずです。

:::note warn
GASでこの記事に書かれたコードを実行すると、初回は「ご利用のGoogleアカウントへのアクアセスを許可する必要があります」という警告が出ます。外部サイトへの接続が含まれるためです。この許可を行おうとすると「このアプリはGoogleで確認されていません」という警告が再度出ます。各コードの意味をご自身で確認した上で、許可を行ってください。
:::


ご参考：Google Apps Scriptのはじめかた

https://qiita.com/ryojio2k/items/3f0310647f2d3b278fe2

# コーディング
## まず、API経由で電球を操作する準備をする
### API仕様書から認証の手順を知る
公開されているSwitchBot API仕様書はこちら。

https://github.com/OpenWonderLabs/SwitchBotAPI

まずは認証（Sign）の準備からです。勝手に他人から電球を操作されてはなりませんから、リクエストが本人から正しく送信されているのかの確認が行われます。

SwitchBotのAPIにおける本人確認は、SwitchBotアカウントのIDとパスワードではなく、「トークン」と「シークレット」で行われます。これらはスマホアプリで確認することができますので、文字列をコピーしておきます。
[トークンの取得方法(SwithcBot公式サイト)](https://support.switch-bot.com/hc/ja/articles/12822710195351-%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%81%AE%E5%8F%96%E5%BE%97%E6%96%B9%E6%B3%95)

認証方法は、実は前述のAPI仕様書にたくさんの言語での事例が書かれているのですが、Google Apps Scriptでの記述はありません。また、言語によってやっていることが微妙に異なるので、何が必要なのか少し迷いますが、基本的に手順は以下のとおりです。
1. 「署名」する
2. 署名情報を含む「ヘッダ」を作る
3. リクエストを送るときにこのヘッダを含める

### 「署名」する関数を書く
「トークンに、現在時刻と、任意の文字列を結合させたもの」に対して、シークレットを使って「署名」します。GASにはこの署名（ハッシュ作成）のための関数が用意されています。

~~~js
//署名する
function generateSign(token, secret, t, nonce){
  var string_to_sign = token + t + nonce;

  //HMAC-SHA256ハッシュ作成 バイナリデータが返る
  var signTerm = Utilities.computeHmacSha256Signature(string_to_sign, secret);
  //base64にエンコード、さらに大文字化
  var sign = Utilities.base64Encode(signTerm).toUpperCase();

  return sign;
}
~~~
（余談）API仕様書中の事例を見ると、string_to_signやsecretの文字列をバイト型にしなければならないのかなど迷いますが、GASのcomputeHmacSha256Signature関数は文字列型を受け取るので問題ありませんでした。バイナリデータを返すので、64進数化し、さらに大文字に変換しておきます。
なお、64進数の表現には64種類の文字が必要ですが、0-9、A-Z、a-z、一部の記号を利用しているとのことです。これをすべて大文字に変換する意図は不明です。

### 「ヘッダ」を作る関数を書く
前述のトークン、現在時刻、任意の文字列、署名結果、これらに加えて、リクエスト本体（「ボディ」あるいは「ペイロード」と呼ばれる）をJSON形式で記述する旨を、リクエストの「ヘッダ」とします。
任意の文字列は、本当に任意のようですが（空白でもよい）、API仕様書の事例の一つとして示されていた、UUID(Universally Unique Identifier)という、現在地の緯度経度と時刻に基づく自動生成文字列を使っています。

SwitchBotアプリからメモしておいたご自分のトークンとシークレットを入れてください。
ヘッダ情報は、GASの「オブジェクト型」にまとめて返します。オブジェクト型は、キーと値のセットを複数まとめられるデータ型です。

:::note warn
コードを自分でしか実行しない前提のため「トークン」と「シークレット」をコード中に直接書いています。これらを他人に知られると、他人からSwitchBotアプリにつながっている各種デバイスが操作できてしまうので、くれぐれも注意してください。他人に知られた場合は、SwitchBotアプリからトークンとシークレットを再設定することもできるようです。
:::

~~~js
//認証情報を含むヘッダを生成する
function generateHeaders(){
  var token = 'ご自分のトークン';
  var secret = 'ご自分のシークレット';
  var t = Date.now().toString(); //文字列にする！
  var nonce = Utilities.getUuid(); //任意の文字列としてUUIDを利用

  var sign = generateSign(token, secret, t, nonce)
  var headers = {
    'Authorization': token,
    sign: sign,
    t: t,
    nonce: nonce,
    'Content-Type': 'application/json',
  };

  return headers;
}
~~~

（余談）キー名をクォーテーションで囲むかどうかも実は迷いどころでした（原則不要だが、予約語と一致するときや、ハイフン等を含むときには必要とのこと）。
なお実装する際、ヘッダにおいてtを文字列型にしなければならないことと、Content-Typeを指定しなければならないことに気づくまで、1時間半ほどかかりました。

### リクエストを送信してみる
まずは簡単なリクエストとして、自分のSwitchBotアプリで管理されているデバイス一覧を取得してみます。「devices」という、以下のURLで表されるAPIが用意されています。
https://api.switch-bot.com/v1.1/devices

devicesは、HTMLプロトコルのGETという「メソッド」に基づくAPIです。このメソッドの指定と、上で作った関数を使って生成した「ヘッダ」（認証情報が含まれています）から、「オプション」を作ります。なお、ヘッダがあるからには「ボディ」（「ペイロード」とも呼ばれる）もあり、ここには具体的なリクエストの引数を入れるものですが、devicesは引数を必要としませんので指定していません。
UrlFetchApp.fetch関数で、APIのURLへオプションを送信します。成功するとJSON形式の返り値が返ってくるので、JSON.parse関数でパースしてオブジェクト型の変数を作っています。さらには、パースされた結果から、デバイスリスト部分を出力しています。

~~~js
//登録されているデバイスリストを出力する（目的のデバイスのidを目視確認する）
function getDeviceList(){
  const API_BASE_URL = 'https://api.switch-bot.com';
  var url = API_BASE_URL + '/v1.1/devices';
  var headers = generateHeaders();

  var options = {
    method: 'get',
    headers: headers,
  }

  var res = UrlFetchApp.fetch(url, options);
  console.log(res);
  
  var parsed = JSON.parse(res.getContentText());
  console.log(parsed);

  devicelist = parsed['body']['deviceList']
  for(i=0; i<devicelist.length; i++){
    console.log(devicelist[i]);
  }

  return parsed;
}
~~~


### 電球のデバイスIDを取得する
最初にデバイス一覧を取得してみた理由は、ボディが必要ないシンプルなAPIであったためでもありますが、コントロール対象となるスマート電球のデバイスIDを取得するためでもあります（デバイスIDはアプリからは確認できないようです）。
デバイス一覧の出力結果から、以下のようにスマート電球（Color Bulb）の存在が確認できます。そのデバイスID（ここでは'XXXXXXXXXXXX'）が、今後、その電球を操作するために必要な情報ですので、メモしておきます。

~~~
{ deviceId: 'XXXXXXXXXXXX',
  deviceName: 'スマート電球 5A',
  deviceType: 'Color Bulb',
  enableCloudService: true,
  hubDeviceId: '' }
~~~

## 次に、API経由で電球の色を変えてみる
### コマンドを送信する関数を書く
色を変えるのに用いるAPIは「commands」です。
https://api.switch-bot.com/v1.1/devices/(device_id)/commands

これ自体は実は、特定のデバイスIDのデバイスに「コマンド」を送るという、汎用的な機能を持つAPIです。
まず、このAPIをGASで汎用的に利用できるよう、任意のコマンドを送るという関数を書いてみました。引数として、対象となるデバイスID、コマンド（例えば"色を変える"）、そのパラメータ（例えば"緑"）を持ちます。

コマンドとパラメータは、「ボディ（ペイロード）」として、いったんオブジェクト型に格納し、JSON形式のデータに変換します。「ヘッダ」作成時にボディはJSON形式であるという指定をしたのは、これに対応します。
「メソッド」指定と、前述したヘッダ、ボディ（ペイロード）をまとめて「オプション」を作ります。なお、こちらから渡すデータが多いAPIはメソッドがGETでなくPOSTになります。

UrlFetchApp.fetch関数で、URLとオプションを送信します。成功するとJSON型の返り値が返ってくるので、JSON.parse関数でパースしています。

~~~js
//デバイスにコマンドを送る
function postCommand(device_id, command, parameter='default'){
  const API_BASE_URL = 'https://api.switch-bot.com';
  var url = API_BASE_URL + '/v1.1/devices/' + device_id + '/commands';
  var headers = generateHeaders();

  var payload = {
    commandType: 'command',
    command: command,
    parameter: parameter,
  }
  var jsonpayload = JSON.stringify(payload);

  var options = {
    method: 'post',
    headers: headers,
    payload: jsonpayload,
  }

  var res = UrlFetchApp.fetch(url, options);
  console.log(res);

  var parsed = JSON.parse(res.getContentText());
  console.log(parsed);

  return parsed;

}
~~~

整理すると、UrlFetchApp.fetchで送るoptionsは以下のような階層構造になっています。

~~~
options #URLのオプション、オブジェクト型
├ method #メソッド指定。'get'または'post'
├ headers #ヘッダ、オブジェクト型
│  ├ Authorization #トークン
│  ├ sign #署名結果
│  ├ t #時刻
│  ├ nonce #任意の文字列
│  └ Content-Type #ボディ（ペイロード）の形式指定
└ payload #ボディ（ペイロード）、JSON形式
   ├ commandType #基本的には'command'
   ├ command #色を変える、とか
   └ parameter #緑、とか
~~~

### 早速電球の操作テストをしてみる
スマート電球に対するコマンドは以下が用意されています。これもAPI仕様書から調べられます。
RGB方式での色と、色温度での色（白熱灯色～蛍光灯色のような）、また明るさが指定できます。

| command | parameter | 備考 |
|:--------|:----------|:-----|
| turnOn  | 不要|
| turnOff | 不要 |
| toggle  | 不要 | On/Offが切り替わる |
| setBrightness| 1-100の数値 | その明るさでOnになる |
| setColor     | RGBを表す文字列（例："255:255:255"）| その色でOnになる |
| setColorTemperature | 色温度を表す2700-6500の数値 | その色でOnになる |


以下のような感じで、先ほどのコマンド送信用の関数を利用して実行してみましょう。ここでは電球の色を緑に設定しています。
~~~js
function test(){
  const device_id = 'ご自身の電球のデバイスID';  //Color Bulb
  var param = '255:255:0';
  var res = postCommand(device_id, 'setColor', param); //色を変える（自動的にOnになる）
  console.log('RGBを'+param+'に設定しました。');
}
~~~

電球の色が変わりましたでしょうか？

### 電球の状態を取得する関数を書く

ただ、ここで電球がOnになってしまうのは実は想定外でした。留守中など、電球がOffになっているときには、色が変化する必要はないからです。今Onであれば電球の色を変える、ということができるよう、電球の今の状態を取得することが必要だとわかりました。
特定のデバイスの状態を取得するAPI「status」が用意されていますので、これを利用する関数を書きます。ボディは不要です。返り値をもう少し処理してもよいのですが、汎用性をもたせるためにパースしただけで返しています。

~~~js
//デバイスの状態を取得する
function getStatus(device_id){
  const API_BASE_URL = 'https://api.switch-bot.com';
  var url = API_BASE_URL + '/v1.1/devices/' + device_id + '/status';
  var headers = generateHeaders();

  var options = {
    method: 'get',
    headers: headers,
  }

  var res = UrlFetchApp.fetch(url, options);
  console.log(res);

  var parsed = JSON.parse(res.getContentText());
  console.log(parsed);

  return parsed;
}
~~~

電球に対しては、以下のような情報が返ってきます。
~~~
{ statusCode: 100,
  body: 
   { version: 'V1.7-1.7',
     power: 'off',
     brightness: 40,
     color: '255:255:0',
     colorTemperature: 0,
     deviceId: 'XXXXXXXXXXXX',
     deviceType: 'Color Bulb',
     hubDeviceId: 'YYYYYYYYYYYY' },
  message: 'success' }
~~~

電球がOnのときだけ色を変えるためには、先ほどのテストのコードにIF文で判定式を追加すればOKです。
~~~js
function test2(){
  const device_id = 'ご自身の電球のデバイスID'  //Color Bulb
  var param = '255:255:0';
  res = getStatus(device_id);
  if(res['body']['power']=='on'){
      var res = postCommand(device_id, 'setColor', param) //色を変える（自動的にOnになる）
      console.log('RGBを'+param+'に設定しました。');
  }
}
~~~

## 卸電力市場価格を取得する

ここはスマート電球の色を変化させることとは独立の話なので、スマート電球の色を株価など他のデータに合わせて変えたいと思っている方は、読み飛ばしてください。

### そもそも卸電力市場単価とは

電力に関する市場が、株式市場とか豊洲市場とかと同じように存在します。この市場の名前が日本卸電力取引所、略称JEPX（そのままジェイ・イー・ピー・エックスと読みます）です。
豊洲市場でマグロだけを売っているわけではないように、JEPXでも複数の商品が扱われています。電力は、9エリア（北海道(電力)、東京(電力)などの旧来の電力会社の区分。沖縄はない）・30分単位（「コマ」と通称される）で取引されています。エリアとコマが異なれば、別の商品です。東京の本日12時～12時30分の電力と、北海道の本日12時～12時30分の電力、あるいは東京の本日13時～13時30分の電力、明日12時～12時30分の電力が、それぞれ異なる商品として扱われるわけです。
これらの商品に対して長期契約や直前の契約などの複数の取引方法が用意されており、それによっても異なる価格がつきます。市場連動の電気料金と言ったときには通常、「スポット市場」で取引されたときの価格を指します。スポット市場は1日前市場とも言われ、ある日の48コマ分の電力が前日の10時頃までに取引されて、その日のエリア別・コマ別の価格が定まります。

### 卸電力市場価格を取得する

スポット市場価格はJEPXのウェブサイトで公開されており、グラフ等を見ることもできますが、データとしては以下のURL（2024は年度）にアクセスしてcsvで取るのが便利だと思われます（APIは用意されていないようです）。csvは行方向に年度初めからの日付・コマ、列方向にエリアが並んでいます。毎日10時過ぎには、翌日の価格を含めて更新されています。

https://www.jepx.jp/market/excel/spot_2024.csv


以下は、そのcsvを取得して、現在に対応するスポット市場価格を取得するコードの例です。API利用の時に使ったのと同じUrlFetchApp.fetch関数を使うとcsvの中身をテキストとして取得することができますので、ファイルをダウンロードする手間は省いて、毎回そのテキストから目的の値を探しています。
csv形式のテキスト処理には、GASの文字列操作関数splitが利用できます。

~~~js
//現在のスポット市場価格を取得する
//　引数はエリア番号 1: 北海道　2:東北　3:東京　4:中部　5:北陸　6:関西　7:中国　8:四国　9:九州
function getCurrentPrice(area=0){
  var t = new Date();
  console.log(t)
  var year = t.getFullYear();
  if(t.getMonth() <= 3){
    year = year - 1;
  }
  var koma = Math.floor((t.getHours() + t.getMinutes()/60)*2)+1

  //スポット市場価格csvの文字列を取得
  var url = 'https://www.jepx.jp/market/excel/spot_' + year.toString() +'.csv'  
  var res = UrlFetchApp.fetch(url);
  var csvdata = res.getContentText()

  //現在コマの行を特定し価格取得
  var rows = csvdata.split("\n"); 
  var data;
  if(rows[rows.length-2][0] == Utilities.formatDate(t, 'JST', 'yyyy/MM/dd')){    //当日までの価格が出ているとき
    data = rows[rows.length-(48-koma)-2].split(',');
  }else{
    data = rows[rows.length-(48-koma)-48-2].split(','); //翌日までの価格が出ているとき
  }
  var price = data[5+area];

  console.log(rows.length, koma, price);
  return price;
}
~~~

## SwitchBotの電球の色を卸電力市場価格連動で変化させる

### 市場価格連動で色を変化させる関数を書く

あとは、卸市場価格の数値に合わせて電球の色のパラメータを作り、色変更のコマンドを送る関数を書くだけです。これが一連のコードでのメイン関数になります。
数値に対してどのような色あるいは明るさを設定するかは、是非ご自身で工夫ください。一例は示しますが、このコードでの色変化は、分かりにくいし美しくありません（当方にセンスがないので…）。完全連動よりも、一定価格を上回ったら・下回ったら色を変える、また1日のなかでの相対値を見るのほうが実用的かもしれません。

~~~js
//スポット市場価格に合わせて電球の色を変える
function main(){
  //色設定
  const maxprice = 30
  const minprice = 0
  // RGBで色を指定する場合
  const maxparam = 255
  const minparam = 0
  // 色温度で色を指定する場合
  // const maxparam = 6500
  // const minparam = 2700
  //デバイス設定
  const device_id = 'XXXXXXXXXXXX'  //Color Bulb
  //エリア設定
  const area = 3;

  //Onのときだけ色を変える
  res = getStatus(device_id);
  console.log(device_id+'は'+res['body']['power']+'です。')
  if(res['body']['power']=='on'){
    var price = getCurrentPrice(area);
    
    //値を色を表すパラメータに変換
    var param = maxparam - (price - minprice) / (maxprice - minprice) * (maxparam - minparam) ;
    if(param > maxparam){
      param = maxparam;
    }else if(param < minparam){
      param = minparam;
    }
    param = Math.floor(param);
    console.log(param);

    // RGBで色を指定する場合
    param = '255:255:'+param;
    postCommand(device_id, 'setColor', param); //色を変える（自動的にOnになる）
    console.log('市場価格が'+price+'円なので、RGBを'+param+'に設定しました。');

    // 色温度で色を指定する場合
    // postCommand(device_id, 'setColortemperature', param) //色を変える（自動的にOnになる）
    // console.log('市場価格が'+price+'円なので、色温度を'+param+'Kに設定しました。')

  }

}
~~~

### 定期実行を設定する

最後に、main関数を定期的に実行するよう、GASの「トリガー」機能（時間主導型）を設定します。
卸電力市場価格は30分ごとに変わるので30分毎に実行すればよさそうなものですが、この方法での定期実行は期間内のいつに実行されるかは決められないので（例えば30分毎に実行する設定をしても、本日12時～12時30分の価格が12時29分に表示されるかもしれない）15分毎などの設定がよさそうです。

ご参考：スクリプトをスケジュールに従って定期的に実行する方法

https://hajiritsu.com/gas-schedule-trigger/

# おわりに

当方は電力業界関係者ですが、スマート家電にも興味があったのでDIYで作ってみました。
全体で200行程度のコードで、白紙からの作成時間は6時間程度でした（本Quiitaの記事を書くほうがずっと時間がかかりました）。本記事の仕組みは時刻手動型のトリガーを使う想定なので、例えばセンサ情報をもとにしたリアルタイムのコントロールはできませんが、自動化が思ったよりも簡単に実現できたことで、APIの可能性を感じました。

肝心の表示色が微妙というDIYならではの完成度ではありますが、電力業界でもこういったスマート家電のコントロールには強い関心がもたれていますので、今後、洗練された実際のサービスとしての提供も広がっていくものと思います。

市場連動の電気料金メニューや卸電力市場については、わかりやすくなるよう書いてみたつもりで、その過程での不正確さについてはご容赦ください。一方、APIのイメージの説明などは、当方が試行錯誤を通じて体感的に理解したものを書いてみたものなので、そもそも不正確なところがあると思いますがこちらもご容赦ください。

## 謝辞
以下記事を見て、SwitchBotのAPIの可能性を知りました。Zennのアカウントを持っていないため「いいね」ができていませんが、この場を借りて御礼申し上げます。

https://zenn.dev/tanny/articles/808487545eb30f

すべての基礎となるSwithcBotのAPI仕様書です。今回は電球しか操作していませんが、多様多種のデバイスがあることがわかります。

https://github.com/OpenWonderLabs/SwitchBotAPI

ほか、各要素について既に素晴らしい解説記事があるものは、本文中でご参考としてリンクを貼らせていただいています。