🧔‍♂️

ア。ーAlexaの運動についてー

に公開

はじめに

どうもー。スペースマーケットの異端審問官、ソヴァクです。
最近Alexaを買いまして、これがとても良くて、もう私の家はアレクサを中心に回っています。

https://www.amazon.co.jp/dp/B09B2RLPLV?ref=MARS_NAV_desktop_hyp&th=1

家を中心にAlexaが回っているのではないので、異端思想には気をつけてくださいねー

もうすっかり夏ですねー☀️
夏になると何が1番困るかわかります?

....

答えは暑さです
結局そういうシンプルなのが1番困るらしいですよ

そんな夏でもすぐに部屋を涼しくできるように、
今回はAlexaのスキルを自作して、声でエアコンを操作する方法を試してみました。

スキルとはAlexa上におけるアプリのようなものです。
全世界に公開もできるし、自分の環境だけで使うこともできます!

作りたいもの

私の家は、Switchbot Hub2を使って、赤外線リモコンを学習させています。
これを使うと、スマホからテレビやエアコンなどを操作できるようになります。

https://www.switchbot.jp/pages/switchbot-hub2

さらにAlexaとSwitchbotを連携させると、声で操作できるようになります。

しかし声で操作できる機能に限りがあり、私が確認できたところでは、
エアコンの電源・温度・モードの切り替えはできますが、
風量を変えたり、1回のコマンドで同時に操作することができませんでした。

私、こう見えてとても繊細でして、
汗だくで帰宅して、暑いから温度を下げて風量をMaxにしても
5分後には体が冷えて寒いと感じたり、
そして温度を上げると、次は暑いと感じたりするめんどくさい体をしております。

なので寒いと感じたら、
「アレクサ、温度を23℃に戻して」と言ったあと、
スマホで風量を3から1に変更するという、2手間がかかります。

そこで考えました。

アレクサに「めっちゃ暑い」と言えば、2℃気温を下げて風量をMaxに、
「ちょっと暑い」と言えば、1℃気温を下げて、風量を中に、
「もうええわ」と言えば、気温を上げず、風量を小にするように設定できないかと。

SwitchbotをAPIで操作する方法は前回紹介したロボット掃除機を動かす方法と同じ要領で
エアコンにもアクセスできそうです。

https://zenn.dev/spacemarket/articles/fd82577a659a43

アレクサのスキルは、Alexa Developer Consoleから作成できます。
発声を受け取って処理するのは、エンジニアの皆さんお馴染みのAWS Lambdaで動作させています。
なので、GASでSwitchbotを動かす関数を組んでおき、
そのエンドポイントをLambdaで呼び出すようにして実現することができそうですね!

このような構成を想定しています。

いざ実装

1. GASでエアコンを操作する

まずはエアコンにコマンドを送り、エアコンの風量・温度を変更するメソッドを作ります。
headersの取得は前回の記事のStep1を参照してください。

function sendCommandToSwitchBot(temp, mode, fanLevel) {
  const deviceId = "02-XXXXX85"; // 連携済みエアコンのデバイスIDです
  const url = `https://api.switch-bot.com/v1.1/devices/${deviceId}/commands`;

  const payload = {
    command: 'setAll',
    parameter: `${temp},${mode},${fanLevel},on`,
    commandType: 'command'
  };

  const options = {
    method: 'post',
    headers: generateSwitchBotHeaders(),
    payload: JSON.stringify(payload)
  };

  const response = UrlFetchApp.fetch(url, options)

  return JSON.parse(response.getContentText());
}

https://github.com/OpenWonderLabs/SwitchBotAPI?tab=readme-ov-file#command-set-for-virtual-infrared-remote-devices

deviceType commandType Command command parameter Description
Air Conditioner command setAll {temperature},{mode},{fan speed},{power state} e.g. 26,1,3,on the unit of temperature is in celsius.modes include 0/1 (auto), 2 (cool), 3 (dry), 4 (fan), 5 (heat).fan speed includes 1 (auto), 2 (low), 3 (medium), 4 (high).power state includes on and off

Docsの通り、parameterに'温度,モード,風量,on'という形でPOSTすることで、
エアコンを設定できますので、引数にtemp, mode, fanLevelを渡すことで、
柔軟に温度,モード,風量を変更するfunctionの完成です!

次にSwitchbot Hub 2のstatusを取得し、現在の室温を取得します。

function getCurrentTemperature() {
  const deviceId = "CEXXXXXXXE4"; // Switchbot Hub 2のデバイスIDです
  const url = `https://api.switch-bot.com/v1.1/devices/${deviceId}/status`;

  const headers = generateSwitchBotHeaders();

  const options = {
    method: 'get',
    headers: headers
  };

  const response = UrlFetchApp.fetch(url, options);
  const data = JSON.parse(response.getContentText());
  const currentTemperature = data.body.temperature;
  Logger.log(currentTemperature);
  return currentTemperature
}

GET /v1.1/devices/${deviceId}/statusでこのようなレスポンスが返ってきますので、
temperatureを返すメソッドにしてあげます。

レスポンス
{
  "statusCode":100,
  "body": {
    "version": "V3.4-2.3",
    "temperature": 23.6,
    "lightLevel": 13,
    "humidity": 62,
    "deviceId": "CEXXXXXXXE4",
    "deviceType": "Hub 2",
    "hubDeviceId": "CE5XXXXA4E4"
  },4
  "message": "success"
}

ここで取得した室温を基準に、

  • 「めっちゃ暑い」-> -2℃
  • 「ちょっと暑い」-> -1℃
  • 「もうええわ」-> 変更せず
    にしたいと思います。
doPost
function doPost(e) {
  const request = JSON.parse(e.postData.contents);
  const intent = request.intent || 'null';

  const fanSettings = {
    veryHot: { delta: -2, fan: FanLevel.HIGH },
    aBitHot: { delta: -1, fan: FanLevel.MEDIUM },
    enough: { delta: 0, fan: FanLevel.LOW }
  };

  const setting = fanSettings[intent];
  if (!setting) {
    return ContentService.createTextOutput(JSON.stringify({ error: 'invalid intent' })).setMimeType(ContentService.MimeType.JSON);
  }

  const currentTemp = getCurrentTemperature();
  const targetTemp = Math.round(currentTemp + setting.delta);

  sendCommandToSwitchBot(targetTemp, AirConMode.COOL, setting.fan)

  return buildAlexaResponse(`${targetTemp}度に設定しました。`);
}

// 風量設定
const FanLevel = Object.freeze({
  AUTO: 1,
  LOW: 2,
  MEDIUM: 3,
  HIGH: 4
});

// エアコンモードの設定
const AirConMode = Object.freeze({
  AUTO: 1,
  COOL: 2,
  DRY: 3,
  FAN: 4,
  HEAT: 5
});

doPostメソッドをAPIとして公開し、
eにアレクサに話されたワードがintentという形で渡されます。

intentの設定は後ほど、Alexa Developer Consoleで作成するので、
ここでは「そういうものがあるのか」くらいに考えてください。

intentの種類は、veryHot, aBitHot, enoughの3種類を想定しており、
それぞれが来たときの、delta(何℃下げるか)、fan(風量設定)を定義しておきます。

getCurrentTemperature()で取得した現在温度から、deltaを足して、
エアコンの温度を設定し、それをSwitchbotにリクエストします。

まず、こちらを手動で実行してみて、エアコンが反応するか確認してみてください。
こちらがうまく動作したら、doPostをデプロイし、
アクセスできるユーザーは「全員」に設定してあげます。

GASの設定は以上です!

2. Alexa Developer Consoleでスキル作成

Amazon Developerにアクセスし、
Alexa Skills Kitからスキルを作成しましょう!

「スキルを作成」へ進むと、スキル名と言語を選択できます。
ここでのスキル名は、アレクサに呼びかけるスキルとは異なりますので、
自分のわかりやすい名前で大丈夫です。

エクスペリエンスのタイプは「スマートホーム」を選択し、
モデルは「カスタム」で作成します。

ホスティングサービスは、今回はNode.jsを選択しました。

最後まで進み、「スキルを作成する」をクリック。

すると日本語だった画面が急に英語になり、難解な言葉が並びますが、あきらめないでー

3. Alexaが言葉を理解できるようにIntentを登録する

ここでAlexaスキルについてですが、
フロント部分はintentと呼ばれるものにワードやフレーズを登録することで、
Alexaがユーザーの発話を理解して、それに応じた処理を実行することができます。

今回のケースでは、VeryHotIntentを作成し「暑すぎる」「めっちゃ暑い」というワードを追加します。

同様に、ABitHotIntentを作成し、「涼しくして」「ちょっと暑い」を追加、
EnoughIntentも作成し、「もうええわ」「涼しくなった」をそれぞれ登録しておきます。

また、Skill Invocation Nameも設定する必要があります。
Skill Invocation Nameとは、先ほど設定した「スキル名」とは異なり、
ユーザーがこのSkill Invocation Nameを呼ぶことで、実際に発動できるスキルの名前になります。

Alexaが言葉を理解して、動作してもらうためには、
このSkill Invocation Name + Intentの登録ワードを連続で言う必要があります。

つまり、「アレクサ、めっちゃ暑い」では反応できず、
「アレクサ、〇〇(Skill Invocation Name)めっちゃ暑い」と言わなければ、理解してくれません。


出展: チ。ー地球の運動についてー 1巻P.46

「アレクサ、部屋の中、めっちゃ暑い」

これで自然な言葉になりましたね。

設定が終わったら忘れずに「Build skill」ボタンをクリックしてください。

4. Lambdaに実装していく

次に、BE部分です。

Alexa Developer Consoleの「コードエディタ」のタブを開くと、
このようなエディタが表示されると思います。

こちらのindex.jsにAlexaがintentを受け取ってからの処理を記載していきます。
(サンプルコードが記載されていますので、一旦全削除して大丈夫です。)

まずは、GASで作成したAPIを呼び出すfunctionです。
https.requestを使って、GASにAlexaからIntent名を含めてリクエストするコードです。

const callGasAPI = (intentKeyword) => {
  const payload = JSON.stringify({ intent: intentKeyword });

  const options = {
    hostname: 'script.google.com',
    path: '/macros/s/hogehoge/exec', // GASのデプロイURL
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Content-Length': payload.length,
    },
  };

  return new Promise((resolve, reject) => {
    const req = https.request(options, (res) => {
      res.setEncoding('utf8');
      let body = '';
      res.on('data', (chunk) => { body += chunk; });
      res.on('end', () => {
        resolve(JSON.parse(body));
      });
    });

    req.on('error', (e) => {
      reject(e);
    });

    req.write(payload);
    req.end();
  });
};

次に、「めっちゃ暑い」や「もうええわ」を聞き取ったときの動作をこちらに定義します。
'VeryHotIntent'をgasに定義した'veryHot'のように変換して、リクエストを投げます。

const createIntentHandler = (intentName, keyword) => ({
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'IntentRequest' &&
           handlerInput.requestEnvelope.request.intent.name === intentName;
  },
  async handle(handlerInput) {
    try {
      await callGasAPI(keyword);
      return handlerInput.responseBuilder
        .speak(`エアコンの設定を変更しました。`)
        .getResponse();
    } catch (e) {
      console.error(e);
      return handlerInput.responseBuilder
        .speak(`エラーが発生しました。`)
        .getResponse();
    }
  }
});

最後に、アレクサスキルのエントリーポイントとなるハンドラーです。

exports.handler = require('ask-sdk-core').SkillBuilders.custom()
  .addRequestHandlers(
    createIntentHandler('VeryHotIntent', 'veryHot'),
    createIntentHandler('ABitHotIntent', 'aBitHot'),
    createIntentHandler('EnoughIntent', 'enough'),
  )
  .lambda();

こちらで実装は以上です!
すべての設定を終えたら、デプロイボタンを押してください。
デプロイ完了すると、自分のAlexaで使えるようになっています。

これで部屋が火あぶりのように暑い日でも
「アレクサ、部屋の中めっちゃ暑い」と言えば急冷してくれ、

涼しくなったなと思えば
「アレクサ、部屋の中もうええわ」
と言えば風量を弱にし、現在の室温をキープしてくれます。

最後に

スペースマーケットでは一緒に働く仲間を募集しています!
カジュアルに話を聞きたいだけという方でも大歓迎ですので、ちょっとでも興味があれば以下からご応募お待ちしております!

スペースマーケット Engineer Blog

Discussion