Python
AWS
lambda
Slack

平日19時にエアコンONにする?と聞く仕組みを作った

はじめに

以前に外出先からエアコンを動かす仕組みを作りました。

しかしながら、実際に使っていると、うっかり忘れてしまうという運用上の欠点が発覚しました(知ってた)。

そこで今回は、予め決まった時間になると「エアコンをONにする?」と聞くようにすることで、うっかり忘れてしまうことを防ぎたいと思います!

全体像

system.png

Lambdaを定期実行し、「エアコンをONにする?」とSlackにボタン付きでPOSTします。
SlackでYesボタンを選択すると、別途用意されているAPI Gatewayを叩くことでエアコンが付きます!

なお、本記事の内容は、赤枠部分になります。赤枠以外は前回の内容そのままです。

環境構築

Slackの環境構築

難しいことは行いません。メッセージを「受信」してアクション結果を「送信」するSlackアプリを作成します。

なお、投稿対象となるチャンネルは準備済みとします。

アプリの作成

Slackの下記サイトを開き、Appを作成します。

slack01.png

適当に作成します。

slack02.png

Incoming Webhooks

「Incoming Webhooks」を選択し、Onにします。

slack03.png

一番下の「Add New Webhooks to Workspace」を選択し、許可を行います。

ここで作成されたWebhooks URLは後に必要になるためメモしておきます。

slack04.png

Interactive Components

続いて、「Interactive Components」を選択し、Onにします。

slack05.png

「Request URL」には、応答先のURLを入力し、「Save Changes」を選択します。

ここの応答先とは、Slack上でボタンを押した際にPOSTする先となります。私の場合は、以前に作成したAPI Gatewayを使い回すため、そのURLを入力しています。

slack06.png

AWSの環境構築

Slackに通知するLambda(定期実行)を作成します。

AWS SAMを使っているため、ポイントだけご紹介します。

Lambda

app.py
import os
import json
import requests

SLACK_WEBHOOK_URL = os.environ["SLACK_WEBHOOK_URL"]

def lambda_handler(event, context):
    post_slack("エアコンをOnにしますか?")

def post_slack(message):
    payload = {
        "attachments": [
            {
                "text": message,
                "callback_id": "ask_aircon",
                "color": "#3AA3E3",
                "attachment_type": "default",
                "actions": [
                    {
                        "name": "aircon",
                        "text": "Yes",
                        "type": "button",
                        "value": "yes"
                    },
                    {
                        "name": "aircon",
                        "text": "No",
                        "type": "button",
                        "value": "no"
                    }
                ]
            }
        ]
    }

    # http://requests-docs-ja.readthedocs.io/en/latest/user/quickstart/
    try:
        response = requests.post(SLACK_WEBHOOK_URL, data=json.dumps(payload))
    except requests.exceptions.RequestException as e:
        print(e)
    else:
        print(response.status_code)

環境変数として下記を設定しています。

Key Value
SLACK_WEBHOOK_URL Incoming WebhooksのWebhooks URL

全貌はこちらをどうぞ。

CloudWatch Events

下記のcron式を使用し、「毎週月~金のAM10時(UTC)」に実行させています。日本時間の19時ですね。帰宅時間!(帰れるとは言ってない)

cron(0 10 ? * MON-FRI *)

Raspberry Pi

以前の作成したRaspberry Piのコードを修正します。

修正結果はこちら。受信データの解析処理を分離させました。

main.py
#長いので省略

def subscribe_callback(client, userdata, message):
    logger.info("Received a new message: ")
    logger.info(message.payload)
    logger.info("from topic: ")
    logger.info(message.topic)

    params = parse_payload.parse(message.payload)
    logger.info(json.dumps(params, indent=4))

    remote_control(params)

def remote_control(params):
    if is_aircon_on(params):
        logger.info("Execute GPIO_AIRCON_PIN")
        execute(GPIO_AIRCON_PIN)

def is_aircon_on(params):
    if params["command"] == "/control" and params["param"] == "aircon":
        return True
    if params["command"] == "ask_aircon" and params["param"] == "yes":
        return True
    return False
parse_payload.py
import json
import urllib.parse as url_parse


def parse(payload):
    format_type = _check_format(payload)

    if format_type == "slash_commands":
        data = _parse_slash_commands(payload)
        return {
            "command": data["command"],
            "param": data["text"],
            "data": data
        }
    elif format_type == "interactive_message":
        data = _parse_interactive_message(payload)
        return {
            "command": data["callback_id"],
            "param": data["actions"][0]["value"],
            "data": data
        }
    else:
        return {}


def _check_format(payload):
    data = url_parse.unquote(payload.decode(encoding="utf-8"))
    if data.startswith("payload="):
        return "interactive_message"
    else:
        return "slash_commands"


def _parse_slash_commands(payload):
    params = {}
    key_value_list = url_parse.unquote(payload.decode(encoding="utf-8")).split("&")
    for item in key_value_list:
        (key, value) = item.split("=")
        params[key] = value
    return params


def _parse_interactive_message(payload):
    data = url_parse.unquote(payload.decode(encoding="utf-8")).lstrip("payload=")
    return json.loads(data)

全貌はこちらをどうぞ。

動作結果

平日の19時になると、Slackにメッセージが来ます。それをiPhoneが通知してくれます。

result01.PNG

通知をタップ、もしくは、Slackを開くと下記のようになっており、「Yes」を選択すればエアコンがONになります!!

result02.PNG

1週間ほど運用してみましたが、エアコンを付け忘れて帰宅しても部屋が暑い、ということは無くなりました!!はっぴー!!

参考