Alexa非対応テレビをAlexa対応にする

公開日時
更新日時

※ この記事の前提としてTV(ディスプレイ)を赤外線で操作できる必要があります

FireTVを買ったので「Alexa、テレビつけて」で起動してもらいたかったのだが、「デバイスが応答ありません」と言われ起動できなかった。

接続しているのがTVではなく液晶ディスプレイなので、対応する赤外線プロファイルが存在せずFireTVから操作ができないらしい。

幸いなことに赤外線リモコン付きのディスプレイなので、IRKitを介せばAlexa経由で操作ができるはず。

ということで下記記事を参考にさせていただき、Alexa非対応テレビをAlexa対応にしてみた。

記事にもあるようにIRKitのIFTTTを使えば手軽に連携できるが「Alexa、テレビをトリガー」だと使いにくいので「Alexa、テレビつけて」で起動できるようにする。

Alexaスキル作成

  • Alexa Developer Consoleで新規にスマートホームスキルを作成
  • 参考記事に沿って自分専用スキルとしてアカウントリンクを実装
    • Lambdaとの連携後に「有効なスキル」に表示されるようになる

Lambda実装

参考記事のコードをベースにTVと暖房を追加した。

また、IFTTTの代わりに家のRaspberryPiにIRKit操作用のAPIを用意してリクエストを送るようにした。

Lambdaから家のRaspberryPiへのアクセスにはinletsを使って対応した

Host名+keyだけのURLだと推測しやすいので、適当に生成したAPI_TOKENをURLに含めるようにした。

生成したAPI_TOKENはLambdaの環境変数に設定しておく。

exports.handler = (request, context) => {
    switch(request.directive.header.namespace) {
      case 'Alexa.Discovery' : 
          handleDiscovery(request, context)
          break
     case 'Alexa.PowerController' : 
          handlePowerControl(request, context)
          break
  }
}

function requestPi(key) {
    let https = require("https")
    return new Promise((resolve, reject) => {
        https.get(`https://{raspberrypi_api_host}/${process.env.API_TOKEN}/${key}`, res => resolve())
            .on('error', error => reject(error))
    })
}

function handlePowerControl(request, context) {
    let powerResult = ""
    let requestResult
    const endpointId = request.directive.endpoint.endpointId
    switch(request.directive.header.name) {
        case 'TurnOn' :
            requestResult = requestPi(`${endpointId}_on`)
            powerResult = "ON"
            break
        case 'TurnOff' :
            requestResult = requestPi(`${endpointId}_off`)
            powerResult = "OFF"
            break
    }
    requestResult.then(() => {
        var responseHeader = request.directive.header
        responseHeader.namespace = "Alexa"
        responseHeader.name = "Response"
        responseHeader.messageId = responseHeader.messageId + "-R"
    
        var response = {
            context: {
                "properties": [{
                    "namespace": "Alexa.PowerController",
                    "name": "powerState",
                    "value": powerResult,
                    "timeOfSample": (new Date()).toISOString(),
                    "uncertaintyInMilliseconds": 50
                }]
            },
            event: {
                header: responseHeader,
                endpoint: {
                    scope: {
                        type: "BearerToken",
                        token: request.directive.endpoint.scope.token
                    },
                    endpointId: endpointId
                },
                payload: {}
            }
        }
        context.succeed(response)  
    })
}

function handleDiscovery(request, context) {
    switch(request.directive.header.name) {
        case 'Discover' :
            let header = request.directive.header;
            header.name = "Discover.Response";
            let payload = {
                    "endpoints": [
                        {
                            "endpointId": 'irkit_tv', 
                            "manufacturerName": "IRKIT",
                            "friendlyName": "テレビ",
                            "description": "テレビのスイッチ",
                            "displayCategories": ["TV"],
                            "capabilities": [
                                {
                                  "type": "AlexaInterface",
                                  "interface": "Alexa",
                                  "version": "3"
                                },
                                {
                                    "interface": "Alexa.PowerController",
                                    "version": "3",
                                    "type": "AlexaInterface",
                                    "properties": {
                                         "retrievable": true
                                    }
                                }
                            ]
                        },
                        {
                            "endpointId": 'irkit_heater',
                            "manufacturerName": "IRKIT",
                            "friendlyName": "暖房",
                            "description": "暖房のスイッチ",
                            "displayCategories": ["THERMOSTAT"],
                            "capabilities": [
                                {
                                  "type": "AlexaInterface",
                                  "interface": "Alexa",
                                  "version": "3"
                                },
                                {
                                    "interface": "Alexa.PowerController",
                                    "version": "3",
                                    "type": "AlexaInterface",
                                    "properties": {
                                         "retrievable": true
                                    }
                                }
                            ]
                        }
                    ]
                }
            context.succeed({
                event: {
                    header: header, 
                    payload: payload
                }
            })
            break
        case 'Alexa.PowerController' : 
            handlePowerControl(request, context)
            break
    }
}

IRKit操作api実装

Lambdaから飛んでくるリクエストに応じてIRKitの赤外線送信を出し分けるAPIを実装。

Lambdaのテスト実行で正しくリモコン操作ができることを確認。

const express = require('express')
const exec = require('child_process').exec
const app = express()
const TOKEN = process.env.API_TOKEN

app.get(`/${TOKEN}/irkit_tv_on`, (req, res) => {
  // display on
  exec('curl -i -s -X POST "http://{irkit_host}/messages" -H "X-Requested-With: curl" -d \'{"format":"raw","freq":xx,"data":[xxxx]}\'')
  res.send('ok')
})

app.get(`/${TOKEN}/irkit_tv_off`, (req, res) => {
  // display off
  exec('curl -i -s -X POST "http://{irkit_host}/messages" -H "X-Requested-With: curl" -d \'{"format":"raw","freq":xx,"data":[xxxx]}\'')
  res.send('ok')
})

app.get(`/${TOKEN}/irkit_heater_on`, (req, res) => {
  // heater on
  exec('curl -i -s -X POST "http://{irkit_host}/messages" -H "X-Requested-With: curl" -d \'{"format":"raw","freq":xx,"data":[xxxx]}\'')
  res.send('ok')
})

app.get(`/${TOKEN}/irkit_heater_off`, (req, res) => {
  // heater off
  exec('curl -i -s -X POST "http://{irkit_host}/messages" -H "X-Requested-With: curl" -d \'{"format":"raw","freq":xx,"data":[xxxx]}\'')
  res.send('ok')
})

app.listen(3000, () => console.log('app listening on port 3000!'))

これで「Alexa、テレビつけて」でテレビがつくようになり、ついでに「Alexa、暖房つけて」で暖房も操作できるようになった。

「Alexa、テレビつけて」

「Alexa、XXXを再生して」

「Alexa、テレビ消して」

という基本的な操作は音声のみで完結できるようになって快適。

今後は、同様にしてデバイスを追加していけば機能拡張できる。

以上でAlexa非対応テレビ(ディスプレイ)をAlexa対応にできた。


Related #raspberry pi

ngrokの代わりにCloudflare Tunnelを使う

botやwebhookを利用するサービスの開発が捗る

Dockerのデータ保存場所を変更する

/etc/docker/daemon.jsonに追記

Alexaにゲーミングマシンと周辺環境を終了してもらう

「Alexa Steamを終了して」ができるようになった

Alexaにゲーミングマシンと周辺環境を起動してもらう

PC起動、TV起動、サウンドバー起動がまとめてできるようになった

docker-compose build時に「no Space Left on Device」が発生

1年前にも同じエラーにハマってた