ちょっと話題の記事

[日本語Alexa] 私の部屋の蛍光灯のリモコンが壊れたので・・・Echoで操作できるようにした

2018.01.18

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

1 私の部屋の蛍光灯のリモコンが壊れました・・・

私の部屋の蛍光灯は、壁とかにスイッチが無く、リモコンでしか操作できなかったのですが、その大事なリモコンが壊れてしまいました。

幸い、2階の部屋の蛍光灯が同じリモコンだったので、一応、凌げるのですが、いちいち取りに行くのは面倒なので・・・Alexaでオン・オフできるようにしてみました。

本記事は、そのレポートです。

2 赤外線リモコンのパターン取得

最初にリモコンの赤外線パターンをコピーしました。写真では、読み取るために赤外線受信機をArduinoに接続しています。

オシロスコープで見ると最大5VのON/OFFが確認できるます。

トリガーを設定してみると、なんとなくパターンらしいものも見れます。

Arduinoでは、赤外線リモコンを扱う便利なライブラリが公開されていますので、今回はそれをそのまま利用させて頂きました。


IRremote Arduino Library

動画は、赤外線パターンを読み取っているようすです。

シリアル接続で取得したデータが、以下のような感じです。+の数字が赤外線ONの時間(ms)で、-の数字がOFFの時間です。ベンダー名とヘキサのコードも認識されています。なお、unsigned int rawData[67] のように、同じパターンを送信する場合に使用できる配列も用意されていて超便利です。

Encoding  : NEC
Code      : 41B658A7 (32 bits)
Timing[67]: 
     +9000, -4300     + 700, - 400     + 700, -1550     + 650, - 450
     + 700, - 450     + 650, - 450     + 650, - 450     + 700, - 450
     + 650, -1550     + 650, -1600     + 650, - 450     + 650, -1550
     + 700, -1550     + 650, - 450     + 700, -1500     + 650, -1600
     + 700, - 400     + 700, - 450     + 650, -1550     + 700, - 400
     + 700, -1550     + 650, -1550     + 650, - 450     + 650, - 500
     + 650, - 450     + 700, -1550     + 650, - 450     + 700, -1500
     + 650, - 500     + 650, - 450     + 700, -1550     + 700, -1500
     + 650, -1550     + 650
unsigned int  rawData[67] = {9000,4300, 700,400, 700,1550, 650,450, 700,450, 650,450, 650,450, 700,450, 650,1550, 650,1600, 650,450, 650,1550, 700,1550, 650,450, 700,1500, 650,1600, 700,400, 700,450, 650,1550, 700,400, 700,1550, 650,1550, 650,450, 650,500, 650,450, 700,1550, 650,450, 700,1500, 650,500, 650,450, 700,1550, 700,1500, 650,1550, 650};  // NEC 41B658A7
unsigned int  data = 0x41B658A7;

3 赤外線のパターン送信

IRremote Arduino Libraryを利用すると、パターンの送信も簡単にできるのですが、Alexaから操作するためにはインターネット上にエンドポイントが必要になるので、最初からEndPointを持ったArduino Spark Coreを使用することにしました。

[参考]
Wi-Fi付きのArduino互換機 Spark Core (その1) 初期設定とTINKERによるアクセス
Wi-Fi付きのArduino互換機 Spark Core (その2) Spark Build と WebAPIによるアクセス
Wi-Fi付きのArduino互換機 Spark Core (その3) Spark Photon 19$

写真は、Spark Coreに赤外線LEDをつないでいるようすです。Spark Coreの端子からのON/OFFしただけでは、ちょっと電流不足で蛍光灯まで届かなかったので、SC1815でドライブしました。

送信するコードについては、IRremoteをSpark Core用にカスタマイズされたものを使用させて頂きました。

https://github.com/qwertzguy/Spark-Core-IRremote

こちらのライブラリは、PWMではなく digitalWrite()delayMicroseconds() でIRパルスを生成しています。パルスの最大周波数は166kHzなので、普通(ほとんどのIRコードは38khz)は、そのままで問題なく使えるはずです。

Spark Codeでは、エンドポイントを自由に設計できます。次のコードでは、int Infrared(String _); というWebAPIを公開しています。(引数及び戻り値は固定)

#include "IRremote.h"

//WebAPIの型宣言
int Infrared(String _); 

IRsend irsend(D3);

void setup()
{
    //WebAPIの公開
    Spark.function("Infrared", Infrared); 
}

void loop() {

}

int khz = 38; // 38kHz carrier frequency for the NEC protocol
unsigned int  irSignal[] = {9000,4300, 700,400, 700,1550, 650,450, 700,450, 650,450, 650,450, 700,450, 650,1550, 650,1600, 650,450, 650,1550, 700,1550, 650,450, 700,1500, 650,1600, 700,400, 700,450, 650,1550, 700,400, 700,1550, 650,1550, 650,450, 650,500, 650,450, 700,1550, 650,450, 700,1500, 650,500, 650,450, 700,1550, 700,1500, 650,1550, 650};

int Infrared(String _){
    int len = sizeof(irSignal) / sizeof(int);
    irsend.sendRaw(irSignal, len, khz); 
    return 1;
}

そして、エンドポイントは、デバイスのIDとトークンでアクセスできます。

$ curl https://api.spark.io/v1/devices/{デバイスID}/{ファンクション名} \
    -d access_token={アクセストークン} \
    -d “パラメータ文字列”

curlで蛍光灯をON/OFFしているようすです。

4 スマートホームスキルの作成

ここまでくれば、後はスマートホームスキルで、エンドポイントを叩くだけです。 Alexa開発者コンソールで「スマートホームスキル」を作成します。

スマートホームスキルでは、アカウントリンクが必須のため、とりあえずLogin with Amazonでアカウントリンクしていますが、今回は、トークンは利用していません。

参考:[Alexa] Login with Amazon との Account Linking で名前を呼びかける挨拶とかメールを送信するスキルを作ってみました

スマートホームスキルは、カスタムスキルと違ってLambdaでしか作成できません。また、トリガーは、Alexa Smart Homeとなります。

Alexaアプリで、作成したスキルを有効にするとアカウントリンクが始まります。

Amazonのアカウントで認可をしておきます。

アカウントリンクの後に表示される端末検出で、「端末の検出」 を選択します。

この時点で、Alexa.Discoveryのディレクティブがスキルに渡され、Lambdaで返したデバイスが表示されることになります。

作成したスマートホームスキルは、以下のとおりです。Discoveryに対しては、Alexa.PowerControllerのデバイスを返し、Alexa.PowerControllerのアクションであるTurnOnTurnOffで、先程作成したエンドポイントを叩いているだけです。

const http = require('request');

exports.handler = function (request, context) {
    if (request.directive.header.namespace === 'Alexa.Discovery' && request.directive.header.name === 'Discover') {
        console.log("Discover request " + JSON.stringify(request));
        handleDiscovery(request, context, "");
    }
    else if (request.directive.header.namespace === 'Alexa.PowerController') {
        if (request.directive.header.name === 'TurnOn' || request.directive.header.name === 'TurnOff') {
            console.log("TurnOn or TurnOff Request " + JSON.stringify(request));
            handlePowerControl(request, context);
        }
    }

    function handleDiscovery(request, context) {
        let payload = {
            "endpoints":
            [
                {
                    "endpointId": "demo_id",
                    "manufacturerName": "SAPPOROWORKS",
                    "friendlyName": "蛍光灯",
                    "description": "私の部屋のリモコンが壊れた蛍光灯",
                    "displayCategories": ["LIGHT"],
                    "capabilities":
                    [
                        {
                            "interface": "Alexa.PowerController",
                            "version": "3",
                            "type": "AlexaInterface",
                            "properties": {
                                "supported": [{
                                    "name": "powerState"
                                }],
                            }
                        }
                    ]
                }
            ]
        };
        let header = request.directive.header;
        header.name = "Discover.Response";
        console.log("Discovery Response: " + JSON.stringify({ header: header, payload: payload }));
        context.succeed({ event: { header: header, payload: payload } });
    }

    function handlePowerControl(request, context) {
        let requestMethod = request.directive.header.name;
        let powerResult;
        if (requestMethod === "TurnOn") {
            powerResult = "ON";
        }
      else if (requestMethod === "TurnOff") {
            powerResult = "OFF";
        }
        let contextResult = {
            "properties": [{
                "namespace": "Alexa.PowerController",
                "name": "powerState",
                "value": powerResult,
                "uncertaintyInMilliseconds": 50
            }]
        };
        let responseHeader = request.directive.header;
        responseHeader.namespace = "Alexa";
        responseHeader.name = "Response";
        responseHeader.messageId = responseHeader.messageId + "-R";
        let response = {
            context: contextResult,
            event: {
                header: responseHeader
            },
            payload: {}

        };
        console.log("Alexa.PowerController " + JSON.stringify(response));

        let deviceId = 'xxxxxxxxxxxxxxxxxx';
        let accessToken = 'xxxxxxxxxxxxxxxxxx';
        let options = {
            url: 'https://api.spark.io/v1/devices/' + deviceId + '/Infrared',
            method: 'POST',
            json: true,
            form: {"access_token":accessToken}
        }
        http(options, function (error, res, body) {
            console.log('STATUS: ' + res.statusCode);
            context.succeed(response);
        })
        console.log('finish');
    }
};

5 最後に

動作している様子は次のとおりです。今回のリモコンは、ボタンが1個で、押すたびに「消灯」「蛍光灯2本」「蛍光灯1本」「豆球」がローテーションするタイプなので、実は、「つけて」と言っても「消して」と言っても同じです。

でも、これで、もう2階までリモコンを取りに行かなくてもいいので嬉しいです。

今回、初めてスマートホームスキルを作成してみました。仕組みが分かってしまえば、結構簡単に作れそうです。先日、赤外線リモコン学習デバイスがAmazonから届いていたので・・・今度は、Arduinoでなく、そっちでちゃんと作ってみたいと思います。