SwitchbotをLINE BOTから操作してみた（Lambda,API Gateway,Go）


# はじめに

自宅を完全スマートホーム化してみたい！！ということで第一弾はSwitchBotの加湿器を買ってみました。

じゃじゃん！
<img width="400" alt="IMG_4786.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2610254/8f96d0b2-7784-0939-272d-d5291f64819d.jpeg">

今回はこの加湿器をLINE上からON/OFFできるようにしてみます。

# 構成

![kousei.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2610254/eb50aeb1-4ed9-4834-f3b1-6b21d7953ad7.png)

# 準備
## LINEアカウント

https://developers.line.biz/ja/docs/messaging-api/getting-started/
を参考にチャネルを作成します。

![スクリーンショット 2022-06-29 15.25.57.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2610254/089328e5-184c-c933-7946-da070488e24e.png)

「チャネル基本設定」のチャネルシークレットと「Messaging API設定」のチャネルアクセストークンをメモしておきます。

## Switchbot
アプリをインストールし、アカウントを作成します。

「デバイスの追加」から加湿器を追加し、好きなように名前をつけます。

<img width="300" alt="IMG_4784.JPG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2610254/6d47e59a-9b7b-8659-def6-d8b546583148.jpeg">

プロフィールの設定のアプリバージョンを10回くらい連続で押すと、「開発者向けオプション」が表示されます。
トークンを取得してメモしておきます。

<img width="300" alt="IMG_4773.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2610254/c6672c41-3696-efc7-1e96-13e2a2b69ba9.jpeg">

# LINEからLambdaを起動してみる
まずは作成したLINEBOTからメッセージを送信するとLambdaが起動するところを目指します。

## Lambda(Go)の作成

関数名とランタイム（Go）を指定し、関数の作成（他の項目はとりあえずデフォルトでOK）

![スクリーンショット 2022-06-29 16.06.16.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2610254/5555ec1f-a0f8-e80d-80e8-6b7897e45d30.png)


ランタイム設定のハンドラを`main`に変更

![スクリーンショット 2022-06-30 18.22.37.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2610254/64a245eb-8700-e6ee-c470-383df05797fb.png)


環境変数を追加(先ほどメモした内容を設定します)
- CHANNEL_SECRET：LINEのチャネルシークレット
- CHANNEL_TOKEN：LINEのチャネルアクセストークン
- SWITCHBOT_TOKEN：Switchbotのトークン

### コードのデプロイ

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/golang-package.html
を参考にデプロイします。

コードは以下です(Lambdaが起動できているか確認するだけのコード)

```c
package main

import (
	"context"
	"log"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
)

func HandleRequest(ctx context.Context, request events.APIGatewayProxyRequest) (string, error) {
	log.Printf("やっほ〜")
	return "やっほ〜", nil
}

func main() {
	lambda.Start(HandleRequest)
}
```

コードができたら以下のコマンドでzipファイルを作成します（Macの場合）

```c
go get github.com/aws/aws-lambda-go/lambda
GOOS=linux go build main.go
zip main.zip main
```
main.zipをコードソースにアップロード
![スクリーンショット 2022-06-30 18.24.39.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2610254/0e596ed2-ea20-2660-092c-71b27b8fa93a.png)

## API Gatewayの作成

Lambdaの「設定」の「トリガー」からトリガーを追加します。

今回は以下で設定します。
- APIタイプ：REST API
- セキュリティ：オープン

![スクリーンショット 2022-06-29 16.24.55.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2610254/880b8760-8ffc-4db9-d0fa-4b701a66cea2.png)

追加したらAPIエンドポイントの値をコピー

![IMG_4832.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2610254/2d8b909e-790d-2745-3e32-e88753db8e12.jpeg)


作成したLINEのチャネルに移動し、Messaging API設定のWebhook URLにAPIエンドポイントをいれ、Webhookの利用にチェックをつけます。

![IMG_4833.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2610254/eaaa06d7-e8fd-8de0-9be6-238e6446b0e4.jpeg)

作成したチャネルのQRコードから友達追加し、適当なメッセージを送信します。

<img width="300" alt="IMG_4770.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2610254/18430f58-fdd6-23a2-d0ca-5f269de41fc9.jpeg">

Lambdaのモニタリングの「CloudWatchのログを表示」に移動して、「やっほ〜」とログ出力されていればOKです。

![スクリーンショット 2022-06-29 16.41.55.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2610254/46caff38-18f3-b5d8-445a-8ce2088d5f30.jpeg)


# switchbotのapiを叩き、その結果をLINE BOTから送信する

## ディレクトリ構成

```c
.
├──adapter
|   └──controller
|       ​​​​└──handler.go
|   └──gateway
|       ​​​​└──switchbot
|          ​​​​└──switchbot.go
|   └──presenter
|       ​​​​└──linebot.go
├──application
|   └──input
|       ​​​​└──input.go
|   └──usecase
|       ​​​​└──interface.go
|       ​​​​└──ope_appliances.go
├──go.mod
└──go.sum
└──main.go
```

SwitchBot APIのGoクライアントを作っている方がいたので、今回はこちらを使用させていただきました。[go-switchbot](https://github.com/nasa9084/)

### handler.go
```c
package controller

import (
	"context"
	"fmt"

	"workspace/smartHome/application/input"
	"workspace/smartHome/application/usecase"

	"github.com/aws/aws-lambda-go/events"
)

type Controller struct {
	u usecase.Usecase
}

func NewController(usecase *usecase.Usecase) *Controller {
	return &Controller{
		u: *usecase,
	}
}

func (c *Controller) Handler(ctx context.Context, request events.APIGatewayProxyRequest) error {

	req, err := input.Unmarshal([]byte(request.Body))
	if err != nil {
		fmt.Printf("Failed to unmarshal event:%v\n", err.Error())
	}
	for _, event := range req.Events {
		err := c.u.OpeAppliances(ctx, event)
		if err != nil {
			fmt.Printf("Failed to reply by linebot:%v\n", err.Error())
		}
	}

	return nil
}
```
### switchbot.go
```c
package switchbot

import (
	"context"
	"fmt"

	switchbot "github.com/nasa9084/go-switchbot"
)

type SwitchBot struct {
	c SwitchbotInterface
}

func NewSwitchBotGateWay(switchbot SwitchbotInterface) *SwitchBot {
	return &SwitchBot{
		c: switchbot,
	}
}

type SwitchbotInterface interface {
	Device() *switchbot.DeviceService
}

func (s *SwitchBot) CheckStatus(ctx context.Context, deviceName string) (string, string, error) {

	deviceId := ""
	powerState := ""
	deviceList, _, err := s.c.Device().List(ctx)
	if err != nil {
		return "", "", fmt.Errorf("Fail to get device list :%w", err)
	}

	for _, d := range deviceList {
		if d.Name == deviceName {
			status, err := s.c.Device().Status(ctx, d.ID)
			if err != nil {
				return "", "", fmt.Errorf("Fail to check the status of power %v:%w", d.Type, err)
			}
			deviceId = d.ID
			powerState = status.Power.ToLower()
			fmt.Printf("Check the status of power: %s is %s\n", d.Type, status.Power.ToLower())
		}
	}

	if deviceId == "" {
		return "", "", fmt.Errorf("No device")
	}
	return deviceId, powerState, nil
}

func (s *SwitchBot) TurnOnOff(ctx context.Context, deviceName string) (string, error) {

	deviceId, powerState, err := s.CheckStatus(ctx, deviceName)
	if err != nil {
		return "", fmt.Errorf("Fail to check the status of power:%w", err)
	}

	if powerState == "on" {
		err = s.c.Device().Command(ctx, deviceId, switchbot.TurnOff())
		powerState = "off"
	} else if powerState == "off" {
		err = s.c.Device().Command(ctx, deviceId, switchbot.TurnOn())
		powerState = "on"
	}
	if err != nil {
		return "", fmt.Errorf("Fail to tunn on/off the power :%w", err)
	}

	fmt.Println("Succeeded in turning on/off the power")
	return powerState, nil
}

```
### linebot.go
```c
package presenter

import (
	"fmt"

	"github.com/line/line-bot-sdk-go/linebot"
)

type Presenter struct {
	l LinebotInterface
}

func NewPresenter(linebot LinebotInterface) *Presenter {
	return &Presenter{
		l: linebot,
	}
}

type LinebotInterface interface {
	ReplyMessage(replyToken string, messages ...linebot.SendingMessage) *linebot.ReplyMessageCall
}

func (p *Presenter) Reply(replyToken string, text string) error {

	if _, err := p.l.ReplyMessage(replyToken, linebot.NewTextMessage(text)).Do(); err != nil {
		return err
	}
	fmt.Println("Reply to line bot")
	return nil
}
```
### input.go
```c
package input

import "encoding/json"

type LineRequest struct {
	Events      []Event `json:"events"`
	Destination string  `json:"destination"`
}

type Event struct {
	Type       string  `json:"type"`
	ReplyToken string  `json:"replyToken"`
	Source     Source  `json:"source"`
	Timestamp  int64   `json:"timestamp"`
	Message    Message `json:"message"`
}

type Message struct {
	Type string `json:"type"`
	ID   string `json:"id"`
	Text string `json:"text"`
}

type Source struct {
	UserID string `json:"userId"`
	Type   string `json:"type"`
}

func Unmarshal(data []byte) (LineRequest, error) {
	var r LineRequest
	err := json.Unmarshal(data, &r)
	return r, err
}
```
### interface.go
```c
package usecase

import (
	"context"
)

type PresenterInterface interface {
	Reply(replyToken string, text string) error
}

type GateWayInterface interface {
	TurnOnOff(ctx context.Context, deviceName string) (string, error)
}
```
### ope_appliances.go
```c
package usecase

import (
	"context"
	"workspace/smartHome/application/input"
)

type Usecase struct {
	g GateWayInterface
	p PresenterInterface
}

func NewUsecase(switchbot GateWayInterface, linebot PresenterInterface) *Usecase {
	return &Usecase{
		g: switchbot,
		p: linebot,
	}
}

func (u *Usecase) OpeAppliances(ctx context.Context, event input.Event) error {

	deviceName := event.Message.Text
	powerState, err := u.g.TurnOnOff(ctx, deviceName)
	response := ""
	if err != nil {
		response = "error:" + err.Error()
	} else {
		response = event.Message.Text + "が" + powerState + "になりました!やったね!"
	}

	err = u.p.Reply(event.ReplyToken, response)
	if err != nil {
		return err
	}

	return nil
}
```
### main.go
```c
package main

import (
	"log"
	"os"
	"workspace/smartHome/adapter/controller"
	"workspace/smartHome/adapter/gateway/switchbot"
	"workspace/smartHome/adapter/presenter"
	"workspace/smartHome/application/usecase"

	sw "github.com/nasa9084/go-switchbot"

	"github.com/aws/aws-lambda-go/lambda"
	"github.com/line/line-bot-sdk-go/linebot"
)

func main() {

	sbcli := sw.New(
		os.Getenv("SWITCHBOT_TOKEN"),
	)
	lbcli, err := linebot.New(
		os.Getenv("CHANNEL_SECRET"),
		os.Getenv("CHANNEL_TOKEN"),
	)
	if err != nil {
		log.Fatal(err)
	}
	presenter := presenter.NewPresenter(lbcli)
	switchbot := switchbot.NewSwitchBotGateWay(sbcli)
	usecase := usecase.NewUsecase(switchbot, presenter)
	handler := controller.NewController(usecase)
	lambda.Start(handler.Handler)
}
```
(clean architectureを絶賛勉強中なので何かご指摘あれば教えてください…！)

こちらのコードをデプロイします。

LINEBOTからデバイスに登録した名前を打ち込むと、加湿器をONすることができました！！

<img width="300" alt="IMG_4774.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2610254/2b2cec23-af41-fc71-ec13-ed9ebdeab23c.jpeg">

<img width="500" alt="IMG_4785.JPG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2610254/c1954438-de7a-2dfc-04d2-624dd2c7019b.jpeg">

登録していない名前を送った場合はエラーが返ってきます。

<img width="300" alt="IMG_4775.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2610254/08afac75-7740-bff1-b5ff-90812ab05dae.jpeg">

# LINEのリッチメニュー

毎回デバイス名を打ち込むのは面倒なのでLINEのリッチメニューを使用します。

https://www.linebiz.com/jp/manual/OfficialAccountManager/rich-menus/
を参考に、デバイス名とアイコン用の画像を保存して反映します。

![スクリーンショット 2022-06-29 17.20.37.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2610254/5b9f1f83-c359-0b97-f1c0-31572ee480bc.png)

アイコンをタップするだけで操作できるようになりました！

<img width="300" alt="IMG_4777.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2610254/a79e0c3a-9962-7b42-d02c-924622a606fd.jpeg">

# 最後に
スマートホーム化第一弾としてSwitchbotを使ってみました。
加湿器以外にも面白そうなデバイスが沢山あるので、色々買ってみてスマートホーム化を目指したいと思います！！
他にもおすすめのものがあれば是非教えてください〜！

# 参考資料

[Messaging APIを始めよう](https://developers.line.biz/ja/docs/messaging-api/getting-started/)
[.zip ファイルアーカイブを使用して Go Lambda 関数をデプロイする](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/golang-package.html)
[API GatewayとLambda(Python)でLINE BOT(Messaging API)開発 [前編]](https://qiita.com/w2or3w/items/1b80bfbae59fe19e2015)
[SwitchBot APIのGoクライアント、go-switchbotを書いた](https://blog.web-apps.tech/go-switchbot/)
[リッチメニューを作成する](https://www.linebiz.com/jp/manual/OfficialAccountManager/rich-menus/)