プログラミング素人のはてなブログ

プログラミングも電気回路も専門外の技術屋の末端が勉強したことや作品をアウトプットするブログ。コードに間違いなど見つけられたら、気軽にコメントください。 C#、Python3、ラズパイなど。

エアコン(のリモコン)をフィジカルハックしてラズパイとSlackでスマートリモコン化する

序論

夏を前にして、暑がりの妻が言いました。

夜寝るとき、エアコンを付けないと暑いし付けっぱなしだと寒いし、タイマーで温度を上げたい。

これに対して、僕としては夜は温度の設定が間違っているだけで、エアコンとは「長期的に快適な温度」を設定すべきと思いましたが、Twitterにもありますように、顧客の意図を「正しく」組みとらなければなりません。

つまり、「暑い寒いを解決すること」ではなく、「暑い寒いを自分の支配下におく」ことがここでの顧客の要望であると考えました。

そこで、ラズパイを使ってこれを実現することを目指しました。

ラズパイではLircや、ArduinoのIRRemotというライブラリがあります。
これらを使って、タイマーで指定時刻にリモコンのキーをエミュレーションして送信すれば実現できると考えられます。
qiita.com

IR Remoteの例
asukiaaa.blogspot.com

しかし、うちのエアコンが悪いのか、プログラムが悪いのか、動作させることはできませんでした。
上手くいかない原因としては、基本周波数が違うとどう頑張ってもダメなことがあるようです。
特にうちで今回ターゲットにしたPanasonicの製品は基本周波数が細かく、厳しいのではないかといううわさがあります。
ちなみにArduinoのIR Remoteではキーコードを一つ入れるとRAMがいっぱいで、つまり一つのキーしか入れられませんでした(Arduino Uno)。
やろうと思っている人は注意です。

そこで、これらのライブラリではなく、動作するリモコンをラズパイでフィジカルハック(物理的ハック)することにしました。

リモコンの回路解析とリード線の作成

制御イメージはこのような感じです。
f:id:s51517765:20180715141718j:plain
スマホからSlackを通して、ラズパイに指示を送り、GPIOを制御してリモコンを作動させます。

リモコンは安く売っているこちらをAmazonで購入しました。

f:id:s51517765:20180708185355j:plain

パターンを目視とテスターで調べ、ボタンは導電性のゴムで導通させることでONされることが分かります。
ちょうどはんだ付けできどうな点があるので、ここにリード線を付けます。
リード線は作業中や運用中に、変に力がかかってしまうと、はんだごとパターンが取れてしまう可能性があるので、ホットボンドで固めておきます。
f:id:s51517765:20180708190033j:plain

回路作成

【図1a】がリモコンの等価回路です。押しボタンの裏には導電性のゴムが付いていて、これにより回路が繋がるとスイッチが動作します。
f:id:s51517765:20180708161649p:plain
これを、電気的に再現するためにトランジスタで回路を作成しました。【図1b】
しかしこれでは、Pin6とPin7がショートしてしまうようでボタンは動作しません。

そこで、トランジスタのエミッタ側に抵抗を入れることで、リモコンのPin同士は完全では無いにしても絶縁をとり、しかもトランジスタが動作するように構成しました。

これで、Pin6とPin7のあいだは460kΩで接続していることになりますが、リモコン的には絶縁のようにふるまいます。
(抵抗値は結構大きくしてもトランジスタは動作するので手持ちから適当に選定)

f:id:s51517765:20180708190958p:plain

試験的にテスト用電源(ただの単三電池を2本直列につないだもの)でBaseに電圧を印加するとリモコンを動作させることができました。
230kΩの抵抗は電池で動作させるときには1kΩでもうまく動いたのですがラズパイでやってみると上手くいかなかったのでできるだけ大きい抵抗にしてみました。

結果的にこれはエミッタフォロワーとなるのかな。

f:id:s51517765:20180715195355j:plain

ラズパイで制御

これをラズパイのGPIOから制御できるか確認します。

TeratermからGPIOを制御して動作確認します。
コマンドラインからGPIOを操作する - IT父さんのロボブログ

#GPIOを出力に設定
$ gpio -g mode 23 out
$ gpio -g mode 24 out
$ gpio -g mode 25 out
#GPIO をHIGH
$ gpio -g write 25 1
#GPIO をLOW
$ gpio -g write 25 0

これで電源スイッチが作動すればOKです。

※ここまで上手くいったかなと思ったのですが、どうしてもラズパイで動かそうとするとリモコンがキー設定モードに入ってしまう現象が発生。制御するボタンを一つにすれば上手くいくのですが、何かいらないところでショートしているのか?仕方ないので制御するボタンを一つにして運用します。もしくはトランジスターではなくフォトカプラにすべきか?

プログラミング

qiita.com
こちらを参考に、slackに話しかけて制御します。
ファイルはslackbot_settings.py→SlackのAPIキーなどの設定、SlackBotPlugin.py→話しかけたときの応答内容、bot.py→Mainのプログラム(これを実行する)airconSet.py→GPIOの制御、を作成します。

SlackBotPlugin.pyには↓のようなキーワードを設定しました。

キーワード 動作
上 up 温度を上げる設定を追加
下 down 温度を下げる設定を追加
リセット 削除 タイマー設定を削除
スタート タイマースタート
ヘルプ これらの制御キーを表示
#SlackBotPlugin.py
# -*- coding: utf-8 -*-
from slackbot.bot import respond_to, listen_to
import re
import time
from datetime import datetime as dt
import airconSet

@respond_to(u'(上|up|下|down)+')
def OrderUpDown(message, something):
    try:
        # someting = 反応したword
        text=message.body['text']
        text=text.replace(something,"") #起動ワードを削除
        timeSet = re.search('[0-9]{4}', text)
        timeSet=timeSet.group(0)
        hh=timeSet[:2]
        mm=timeSet[2:]
        hh=int(hh)
        mm=int(mm)
        if hh>24:
            raise ValueError("error!")
        if mm>59:
            mm=59
        text = re.sub('[0-9]{4}',"", text)
        tempertureSet =re.search('[0-9]{1}',text)
        tempertureSet=tempertureSet.group(0)

        # 命令を出したユーザ名を取得
        userID = message.channel._client.users[message.body['user']][u'name']

        tdatetime = dt.now()
        if something=="up" or something=="上":
            airconSet.set(hh,mm,tempertureSet,"up")
            message.reply("時刻" + timeSet + "に" + tempertureSet + "℃上げるようにセットしました。")
        elif something=="down" or something=="下":
            airconSet.set(hh,mm,tempertureSet,"down")
            message.reply("時刻" + timeSet + "に" + tempertureSet + "℃下げるようにセットしました。")
        print(tdatetime, "accept oder from ", userID, )
    except Exception as e:
        print(e)
        message.reply("タイマーセットの入力書式は hhmm temp up/down です")


@respond_to(u'(reset|リセット|削除|delete)+')
def timerReset(message, something):
    try:
        text = message.body['text']
        airconSet.timer_remove()
        message.reply("タイマー設定を削除しました。")
    except Exception as e:
        print(e)
        message.reply("指示を解釈できませんでした。")

@respond_to(u'(start|スタート)+')
def timerStart(message, something):
    try:
        text = message.body['text']
        message.reply("タイマースタートしました。")
        airconSet.timer()
    except Exception as e:
        print(e)
        message.reply("指示を解釈できませんでした。")

@respond_to(u'(help|ヘルプ|助け)+')
def help(message, something):
    try:
        message.reply("タイマーセット→ hhmm temp up/down、 リセット、スタート")
    except Exception as e:
        print(e)
        message.reply("指示を解釈できませんでした。")

GPIOの制御はwiringpiを使いました。
wiringpiはラズパイにはデフォルトでインストールされていますが、wiringpi2をインストールする必要があります。

$ sudo pip3 install wiringpi2

wiringpi2をインストールしないと、import wiringpiでErrorがでてしまいます。

以前の記事には(自分で)記載していないがそうだったのかな?試行錯誤している間にインストールしたことを忘れたのかもしれないし、ラズビアンのVersionによっても違うのかもしれません。
s51517765.hatenadiary.jp

トランジスタに繋がったGPIO 3つをOUTPUTモードにし、タクトスイッチをINPUT PULLUPになったGPIOに接続します。
プルアップはラズパイの内部プルアップを使います。プルアップされたタクトスイッチをONすると電圧がLowになります。

このほかに、動作設定をslackから呼ばれた時にスペース区切りでテキストファイルに設定を記録します。

#airconSet.py
# -*- coding: utf-8 -*-
import wiringpi
import time
import re
from datetime import datetime as dt

# OUTPUT
PIN_UP = 23  # BCM No
PIN_DOWN = 24
PIN_POWER = 25
PIN_RESET = 15

# GPIO初期化
wiringpi.wiringPiSetupGpio()
# GPIOを入力モード(0)/出力モード(1)に設定
wiringpi.pinMode(PIN_UP, 1)
wiringpi.pinMode(PIN_DOWN, 1)
wiringpi.pinMode(PIN_POWER, 1)
wiringpi.digitalWrite(PIN_RESET, 0)

# 出力をLow
wiringpi.digitalWrite(PIN_UP, 0)
wiringpi.digitalWrite(PIN_DOWN, 0)
wiringpi.digitalWrite(PIN_POWER, 0)

#ボタン押下時間
wait=5
onWait=0.3

print("Aircon set import!")

def timer():
    print("タイマースタート")
    file = open('airconTimer.txt', 'r', encoding='utf')
    text="up"
    try:
        while "up" in text or "down" in text:
            text = file.readline()
            splitText = re.split(" ", text)
            if "up" in text:
                key = 1
            elif "down" in text:
                key = -1
            else: #キーワードがなければ
                break
            hh = int(splitText[0])
            mm = int(splitText[1])
            tempertureSet = int(splitText[2])
            print("up or down =", str(key), "timer=", str(hh), str(mm), "temp=", str(tempertureSet))
    except Exception as e:
        print(e)
    file.close()

    while (wiringpi.digitalRead(PIN_RESET)==1): #プルアップしているのでスイッチ押下でLOW
        time.sleep(0.5)
        tdatetime = dt.now()
        HH = int(tdatetime.strftime('%H'))  # 時刻
        MM = int(tdatetime.strftime('%M'))
        if HH==hh and MM==mm:
            if key==1:
                aircon_temp_set_up(tempertureSet)
            elif key==-1:
                aircon_temp_set_down(tempertureSet)
    print("タイマーストップ")

def timer_remove():
    file = open('airconTimer.txt', 'w', encoding='utf')
    file.close()

def set(hh,mm ,number,updown):
    print("Aircon order!")
    print("time= ",hh,mm)
    print("set= ", number)
    file = open('airconTimer.txt', 'a', encoding='utf')  # 追記モードでオープン
    file.write(str(hh)+" "+str(mm)+" "+str(number)+" "+updown+"\n")
    file.close()

def aircon_power():
    print("Aircon power!")
    wiringpi.digitalWrite(PIN_POWER, 1)
    time.sleep(onWait)
    wiringpi.digitalWrite(PIN_POWER, 0)
    time.sleep(wait)

def aircon_temp_set_up(count):
    tdatetime = dt.now()
    print(tdatetime)
    print("aircon_temp_set_up")
    for i in range(count):
        wiringpi.digitalWrite(PIN_UP, 1)
        time.sleep(onWait)
        wiringpi.digitalWrite(PIN_UP, 0)
        time.sleep(wait)
    print("temp set complete!")
    time.sleep(60)

def aircon_temp_set_down(count):
    tdatetime = dt.now()
    print(tdatetime)
    print("aircon_temp_set_down")
    for i in range(count):
        wiringpi.digitalWrite(PIN_DOWN, 1)
        time.sleep(onWait)
        wiringpi.digitalWrite(PIN_DOWN, 0)
        time.sleep(wait)
    print("temp set complete!")
    time.sleep(60)

def initialize_GPIO():
    # GPIO初期化
    wiringpi.wiringPiSetupGpio()
    # GPIOを入力モード(0)/出力モード(1)に設定
    wiringpi.pinMode(PIN_UP, 1)
    wiringpi.pinMode(PIN_DOWN, 1)
    wiringpi.pinMode(PIN_POWER, 1)
    wiringpi.digitalWrite(PIN_RESET, 0)
    #出力をLow
    wiringpi.digitalWrite(PIN_UP,0)
    wiringpi.digitalWrite(PIN_DOWN, 0)
    wiringpi.digitalWrite(PIN_POWER, 0)

完成

f:id:s51517765:20180715134419p:plain

部品リスト

部品 数量
NPNトランジスタ 3
抵抗 5k 2
抵抗230k 2
ボタン電池ボックス 1
ピンヘッダメス 5穴
ジャンパワイヤ 5
リード線 少々
ユニバーサル基板 1

Raspberry Pi Zero W Starter Kit

Raspberry Pi Zero W Starter Kit