はじめに
自宅を完全スマートホーム化してみたい!!ということで第一弾はSwitchBotの加湿器を買ってみました。
今回はこの加湿器をLINE上からON/OFFできるようにしてみます。
構成
準備
LINEアカウント
https://developers.line.biz/ja/docs/messaging-api/getting-started/
を参考にチャネルを作成します。
「チャネル基本設定」のチャネルシークレットと「Messaging API設定」のチャネルアクセストークンをメモしておきます。
Switchbot
アプリをインストールし、アカウントを作成します。
「デバイスの追加」から加湿器を追加し、好きなように名前をつけます。
プロフィールの設定のアプリバージョンを10回くらい連続で押すと、「開発者向けオプション」が表示されます。
トークンを取得してメモしておきます。
LINEからLambdaを起動してみる
まずは作成したLINEBOTからメッセージを送信するとLambdaが起動するところを目指します。
Lambda(Go)の作成
関数名とランタイム(Go)を指定し、関数の作成(他の項目はとりあえずデフォルトでOK)
ランタイム設定のハンドラをmain
に変更
環境変数を追加(先ほどメモした内容を設定します)
- CHANNEL_SECRET:LINEのチャネルシークレット
- CHANNEL_TOKEN:LINEのチャネルアクセストークン
- SWITCHBOT_TOKEN:Switchbotのトークン
コードのデプロイ
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/golang-package.html
を参考にデプロイします。
コードは以下です(Lambdaが起動できているか確認するだけのコード)
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の場合)
go get github.com/aws/aws-lambda-go/lambda
GOOS=linux go build main.go
zip main.zip main
API Gatewayの作成
Lambdaの「設定」の「トリガー」からトリガーを追加します。
今回は以下で設定します。
- APIタイプ:REST API
- セキュリティ:オープン
追加したらAPIエンドポイントの値をコピー
作成したLINEのチャネルに移動し、Messaging API設定のWebhook URLにAPIエンドポイントをいれ、Webhookの利用にチェックをつけます。
作成したチャネルのQRコードから友達追加し、適当なメッセージを送信します。
Lambdaのモニタリングの「CloudWatchのログを表示」に移動して、「やっほ〜」とログ出力されていればOKです。
switchbotのapiを叩き、その結果をLINE BOTから送信する
ディレクトリ構成
.
├──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
handler.go
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
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
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
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
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
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
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することができました!!
登録していない名前を送った場合はエラーが返ってきます。
LINEのリッチメニュー
毎回デバイス名を打ち込むのは面倒なのでLINEのリッチメニューを使用します。
https://www.linebiz.com/jp/manual/OfficialAccountManager/rich-menus/
を参考に、デバイス名とアイコン用の画像を保存して反映します。
アイコンをタップするだけで操作できるようになりました!
最後に
スマートホーム化第一弾としてSwitchbotを使ってみました。
加湿器以外にも面白そうなデバイスが沢山あるので、色々買ってみてスマートホーム化を目指したいと思います!!
他にもおすすめのものがあれば是非教えてください〜!
参考資料
Messaging APIを始めよう
.zip ファイルアーカイブを使用して Go Lambda 関数をデプロイする
API GatewayとLambda(Python)でLINE BOT(Messaging API)開発 [前編]
SwitchBot APIのGoクライアント、go-switchbotを書いた
リッチメニューを作成する