セミオート・スマートホームをつくる その3 IRKitでエアコン制御

semi-smart-home-3

さてさて、前回まででXBeeドア開閉センサから、各窓・ドアの開閉状態をCoordinatorのXBee( ←Arduino Fioに接続)に集約するところまでやりました。今回は、このArduino FioからI2CでRaspberry Piに定期的に情報を引っ張ってきて、その結果を元にIRKitirMagicianでエアコンの制御を行うようにします。

これは好みの問題かもしれませんが、Raspberry PiがArduino Fioから引っ張ってきたセンサ情報は、Web APIで簡単に取得できるようにしておくと、何かと便利かなあと思います。自分はRaspberry Pi上でbottleフレームワークを使ってWebサーバを立てて、例えば”http://192.168.24.50:10080/v1/robots/rapiro/sensors?place=living&device=door&function=null”のようにURLを叩けばセンサ情報をJSONでとれるようにしています。その部分だけ抜粋すると、こんな感じです。

import smbus
bus = smbus.SMBus(1)
ARDUINO_ADDRESS = 0x21

registerMap = {
  "temperature_t":[0xF0,0xF1],
  "humidity_t":   [0xF2,0xF3],
  "illuminance_t":[0xF4,0xF5],
  "mic_t":        [0xF6,0xF7],
  "motion_t":     [0xF8,0xF9],
  "temperature_l":[0xE0,0xE1],
  "humidity_l":   [0xE2,0xE3],
  "illuminance_l":[0xE4,0xE5],
  "motion_l":     [0xE6,0xE7],
  "door_l":       [0xE8,0xE9],
  "window_l":     [0xEA,0xEB],
  "door_l_jp":    [0xEC,0xED],
  "door_e":       [0xD0,0xD1]
}

def read_value(sensor):
  time.sleep(0.01)
  bus.write_byte(ARDUINO_ADDRESS,registerMap[sensor][0])
  time.sleep(0.01)
  data_1 = bus.read_byte(ARDUINO_ADDRESS)
  bus.write_byte(ARDUINO_ADDRESS,registerMap[sensor][1])
  time.sleep(0.01)
  data_2 = bus.read_byte(ARDUINO_ADDRESS)
  #print data_1
  #print data_2
  data = ((data_2 << 8) | data_1)
  if sensor in ['temperature_t','humidity_t','mic_t','temperature_l','humidity_l']:
    data = data*1.0/100
  return data

def sensor_response_json(place, device, value):
  obj = {'place':place, 'device':device, 'value':value}
  return json.dumps(obj)

@route('/v1/robots/rapiro/sensors')
def control_sensors():
  place     = request.query.place.lower()
  device    = request.query.device.lower()
  function  = request.query.function.lower()
  print "Python: Place:" + place + ", Device:" + device

  if place == "entrance":
    if device == "door":
      res = sensor_response_json(place, device, read_value('door_e'))
  elif place == "living":
    if device == "temperature":
      res = sensor_response_json(place, device, read_value('temperature_l'))
    elif device == "humidity":
      res = sensor_response_json(place, device, read_value('humidity_l'))
    elif device == "illuminance":
      res = sensor_response_json(place, device, read_value('illuminance_l'))
    elif device == "motion":
      res = sensor_response_json(place, device, read_value('motion_l'))
    elif device == "door":
      res = sensor_response_json(place, device, read_value('door_l'))
    elif device == "window":
      res = sensor_response_json(place, device, read_value('window_l'))
    elif device == "door_jp":
      res = sensor_response_json(place, device, read_value('door_l_jp'))
  elif place == "room_t":
    if device == "temperature":
      res = sensor_response_json(place, device, read_value('temperature_t'))
    elif device == "humidity":
      res = sensor_response_json(place, device, read_value('humidity_t'))
    elif device == "illuminance":
      res = sensor_response_json(place, device, read_value('illuminance_t'))
    elif device == "motion":
      res = sensor_response_json(place, device, read_value('motion_t'))
    elif device == "mic":
      res = sensor_response_json(place, device, read_value('mic_t'))
  return res

これを踏まえた上で、別途、定期的にWeb APIを叩いてセンサ情報を取得するPythonプログラムを走らせます。位置付けとしては、こんな感じです。

semi-smart-home-3

このプログラムでは、取得したセンサ情報をログとして書き出すと同時に、特定の曜日・時間になったときだけ走らせる処理も書いていきます。今回は、

  • 月〜土曜日の朝6時25分(起床の5分前)に、3つのドアセンサで閉状態が検出されていれば、エアコンをON
  • 朝7時5分になったらエアコンをOFF(消し忘れ防止)

という処理にしたいと思います。このプログラムを、cronを使って毎分ごとに実行させるようにします。

 

まずエアコンを制御するために、IRKitにアクセスできるようにします。IRKitのIPアドレスを調べる方法は、公式ページに書かれています。

$ dns-sd -B _irkit._tcp
Browsing for _irkit._tcp
DATE: ---Sat 25 Jul 2015---
20:02:00.537  ...STARTING...
Timestamp     A/R    Flags  if Domain               Service Type         Instance Name
20:02:01.922  Add        2   5 local.               _irkit._tcp.         irkit90b0
^C
$ dns-sd -G v4 irkit90b0.local
DATE: ---Sat 25 Jul 2015---
20:02:11.235  ...STARTING...
Timestamp     A/R Flags if Hostname                               Address                                      TTL
20:02:11.236  Add     2  5 irkit90b0.local.                       192.168.24.74                                15
^C
Mac-mini:~ nick$

自分はMac使いなので、ターミナルからそのままBonjourを使うことができました。

次に、エアコンをONにするための信号情報を取得します。IRKitに向け「冷房ON」のリモコン信号を出したら、ターミナルで以下を実行します。

$ curl -i "http://192.168.24.74/messages"
HTTP/1.0 200 OK
Access-Control-Allow-Origin: *
Server: IRKit/1.3.4.0.gf19189d
Content-Type: text/plain

{"format":"raw","freq":38,"data":[6424,3341,843,2451,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,2451,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,2451,843,935,843,2451,843,2451,843,2451,843,2451,843,2451,843,2451,843,935,843,2451,843,2451,843,2451,843,2451,843,2451,843,2451,843,2451,843,2451,843,2451,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,2451,843,2451,843,935,843,935,843,2451,843,2451,843,2451,843,2451,843,935,843,935,843,2451,843,2451,843,935,843,935,843,2451,843,935,843,935,843,2451,843,935,843,2451,843,935,843,2451,904,904,904,2451,904,2451,904,904,904,2451,904,904,904,2451,904,904,904,2451,904,2451,904,904,904,904,904,2451,904,904,904,904,904,904,904,904,904,904,904,2451,904,2451,904,904,904,2451,904,2451,904,2451,904,904,904,904,904,2451,904,904,904,904,904,2451,904,2451,904,904,904,2451,904,2451,904,904,904,2451,904,2451,904,904,904,904,904,2451,904,904,904,904,904,904,904,904,904,904,904,904,904,904,904,904,904,2451,904,2451,904,2451,904,2451,904,2451,904,2451,904,2451,904,2451,904,904,904,904,904,904,904,904,904,904,904,904,904,904,904,904,904,2451,904,2451,904,2451,904,2451,904,2451,904,2451,904,2451,904,2451,904,904,904,904,904,904,904,904,904,904,904,904,904,904,904,904,904,2451,904,2451,904,2451,904,2451,904,2451,904,2451,904,2451,904,2451,904,904,904,904,904,904,904,904,904,904,904,904,904,904,904,904,904,2451,904,2451,904,2451,904,2451,904,2451,904,2451,904,2451,904,2451,904,904,904,904,904,904,904,904,904,904,904,904,904,904,904,904,904,2451,904,2451,904,2451,904,2451,904,2451,904,2451,904,2451,904,2451,904,2451,904,2451,904,904,904,904,904,2451,904,904,904,2451,843,935,843,935,843,935,843,2451,843,2451,843,935,843,2451,843,935,843,2451,843,2451,843,935,843,935,843,935,843,2451,843,2451,843,2451,843,2451,843,935,843,2451,843,2451,843,2451,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,2451,843,2451,843,2451,843,2451,843,2451,843,2451,843,2451,843,2451,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,2451,843,2451,843,2451,843,2451,843,2451,843,2451,843,2451,843,2451,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,2451,843,2451,843,2451,843,2451,843,2451,843,2451,843,2451,843,2451,843,935,843,2451,843,2451,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,2451,843,2451,843,2451,843,2451,843,2451,843,2451,843,2451,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,2451,843,2451,843,2451,843,2451,843,2451,843,2451,843,2451,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,2451,843,2451,843,2451,843,2451,843,2451,843,2451,843,2451,843,2451,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,2451,843,2451,843,2451,843,2451,843,2451,843,2451,843,2451,843,2451,843,2451,843,2451,843,2451,843,2451,843,935,843,935,843,935,843,2451,843,935,843,935,843,935,843,935,843,2451,843,2451,843,2451,843,935,843,2451,843,2451,843,2451,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,935,843,2451,843,2451,843,2451,843,2451,843,2451,843,2451,843,935,843,935,843,2451,843,2451,843,935,843,935,843,935,843,935,843,2451,843,2451,843,935,843,935,843,2451,843,2451,843,2451,843,935,843,935,843,2451,843,935,843,2451,843,935,843,935,843,935,843,2451,843,2451,843,935,843,2451,843,935,843,2451,843,2451,843,2451,843,935,843,2451,843,2451,843,935,843,2451,843,2451,843,935,843,935,843,2451,843,935,843,935,843,2451,843,935,843,935,843,2451,843,2451,843,65535,0,65535,0,62212,6648,3458,735,2537,787,1037,735,1150,686,1150,686,1150,686,1150,787,1037,787,1037,686,1150,686,1150,686,1037,843,1037,686,2626,686,1073,787,1073,787,1073,787,1073,686,1150,787,1002,787,1002,787,1150,735,1150,735,1037,735,1150,663,2626,686,1150,663,1150,663,1150,735,1073,735,1073,735,2626,787,1073,619,1073,710,2626,710,2626,710,2626,521,2718,663,2718,663,1190,578,2718,686,2718,686,2718,686,2718,686,2718,686,2718,686,2718,686,2718,686,2718,686,1150,761,1002,735,1150,663,1150,787]}

。。。さすが、エアコンの信号は複雑です。これで冷房ONの信号情報を取得することができました。同様に、エアコンOFFの信号も取得しておきます。

あとは、取得した信号情報のJSONを特定の日時になったらPOSTするようにしてやればOKです。プログラムとしては、以下のようになります。

# coding: utf-8
import requests
import threading
import datetime
import os
import time

LOGGING_INTERVAL = 60

LIVING_AIRCON_OFF = '{"format":"raw","freq":38,"data":[6424,..(省略)..,843]}'
LIVING_AIRCON_COOLER_ON = '{"format":"raw","freq":38,"data":[6424,..(省略)..,3521]}'

def schedule_logging(date,weekday,time,message):
  log_file = open("/home/pi/bottle/log/schedule_log.csv","a")
  log_file.write('{0},{1},{2},{3}\n'.format(date,weekday,time,message))
  log_file.close()

temperature_t = requests.get('http://192.168.24.50:10080/v1/robots/rapiro/sensors?place=room_t&device=temperature&function=null').json()['value']
humidity_t    = requests.get('http://192.168.24.50:10080/v1/robots/rapiro/sensors?place=room_t&device=humidity&function=null').json()['value']
illuminance_t = requests.get('http://192.168.24.50:10080/v1/robots/rapiro/sensors?place=room_t&device=illuminance&function=null').json()['value']
motion_t      = requests.get('http://192.168.24.50:10080/v1/robots/rapiro/sensors?place=room_t&device=motion&function=null').json()['value']
temperature_l = requests.get('http://192.168.24.50:10080/v1/robots/rapiro/sensors?place=living&device=temperature&function=null').json()['value']
humidity_l    = requests.get('http://192.168.24.50:10080/v1/robots/rapiro/sensors?place=living&device=humidity&function=null').json()['value']
illuminance_l = requests.get('http://192.168.24.50:10080/v1/robots/rapiro/sensors?place=living&device=illuminance&function=null').json()['value']
motion_l      = requests.get('http://192.168.24.50:10080/v1/robots/rapiro/sensors?place=living&device=motion&function=null').json()['value']
door_l        = requests.get('http://192.168.24.50:10080/v1/robots/rapiro/sensors?place=living&device=door&function=null').json()['value']
door_l_jp     = requests.get('http://192.168.24.50:10080/v1/robots/rapiro/sensors?place=living&device=door_jp&function=null').json()['value']
window_l      = requests.get('http://192.168.24.50:10080/v1/robots/rapiro/sensors?place=living&device=window&function=null').json()['value']

date  = datetime.datetime.today()
today = date.strftime('%Y-%m-%d')
now   = date.strftime('%H:%M')
weekday = date.strftime('%a')

file_path = date.strftime('/home/pi/bottle/log/%Y-%m-%d.csv')
if  os.path.exists(file_path):
  log_file = open(file_path, "a")
else:
  log_file = open(file_path, "w")
  log_file.write('date,weekday,time,temperature_t,humidity_t,illuminance_t,motion_t,temperature_l,humidity_l,illuminance_l,motion_l,door_l,door_l_jp,window_l\n')
log_file.write('{0},{1},{2},{3},{4},{5},{6},{7},{8},{9},{10},{11},{12},{13}\n'
               .format(today,weekday,now,temperature_t,humidity_t,illuminance_t,motion_t,temperature_l,humidity_l,illuminance_l,motion_l,door_l,door_l_jp,window_l))
log_file.close()

#####################################################################################

if (now == '03:00') and (weekday in ['Mon','Tue','Wed','Thu','Fri','Sat','Sun']):
    payload = {'place':'roomt_t','device':'airconditioner','function':'power','parameter':'on'}
    res = requests.get('http://192.168.24.50:10080/v1/robots/rapiro/appliances', params=payload)
    print(res)
    schedule_logging(today,weekday,now,"ROOM_T_AIRCON_MODE_COOL_POWER_ON")
if (now == '03:02') and (weekday in ['Mon','Tue','Wed','Thu','Fri','Sat','Sun']):
    payload = {'place':'roomt_t','device':'airconditioner','function':'timer','parameter':'off'}
    res = requests.get('http://192.168.24.50:10080/v1/robots/rapiro/appliances', params=payload)
    print(res)
    schedule_logging(today,weekday,now,"ROOM_T_AIRCON_TIMER_OFF_1H")

if (now == '06:25') and (weekday in ['Mon','Tue','Wed','Thu','Fri','Sat']):
  if  door_l == 0 and door_l_jp == 0 and window_l == 0:
    print("All doors and windows are closed. Try to turn on air conditioner.")
    res = requests.post('http://192.168.24.74/messages', data=LIVING_AIRCON_COOLER_ON)
    print(res)
    schedule_logging(today,weekday,now,"LIVING_AIRCON_COOLER_ON")
  else:
    print("Some doors or windows are opened.")
    schedule_logging(today,weekday,now,"NOT_LIVING_AIRCON_COOLER_ON")

if (now == '07:05'):
  print("Try to turn off air cinditioner.")
  res = requests.post('http://192.168.24.74/messages', data=LIVING_AIRCON_OFF)
  print(res)
  schedule_logging(today,weekday,now,"LIVING_AIRCON_OFF")

#####################################################################################

自分はセンサ情報の取得をWeb APIで行っていますが、このあたりは各々好きなように置き換えてもらえばよいと思います。

このプログラムを1分ごとに実行すれば、そのたびに

  • センサ情報の取得
  • センサ情報のCSVファイルへの追記(日が変わると新規ファイル作成)
  • その時間に必要な処理がないかのチェック

が実行されることになります。なお、ここではリビングのエアコン制御とは別に、3:00と3:02に寝室のエアコンをirMagicianで制御するコードも一緒に書いています。

このプログラムを1分ごとに実行するためのcrontabの設定は、こんな感じです。

$ crontab -e
* * * * * python /home/pi/bottle/sensor_logger.py

必要に応じて、こちらを参考にしてもらって、事前にcrontabの編集に使うエディタを使い慣れたものに変更しておくと作業が捗ります。自分はnanoを使えないので、vimに変更しました。

crontabの編集が完了したら、Raspberry Piを再起動させます。これで設定したcronが有効になり、1分ごとにセンサ情報がファイルに記録されるようになるハズです。そして、特定の時間になれば、センサ情報の値を確認して実際にエアコンの制御をするか否かを決めます。今回はシンプルに3つのドアの開閉状態だけを見ていますが、ここまで作り込んでしまえば、特定の温度を上回ったらとか、明るさがどれぐらいになったらとか、そういったきめ細かい制御も、条件を追加するだけで簡単に実現することができます。

 

さてさてこれで、当初の目標は達成です。実際に運用していますが、なかなかに快適です。ちょっとした一手間を減らしているだけ、と言えばそれまでですが、スマート・ホームは結局、その一手間をどれだけたくさん減らせるか、というものだと思っています。

 

次回は、この取り組みから派生して、蓄積したセンサログ情報をどうやって見える化するか、というところをやりたいと思います。