39Si

プログラミング関連の勉強した内容を簡単にまとめておきます

【Apple Watch と IoT】 Apple Watch でエアコンを制御する【前編】

前置き

昨年、Apple Watch が発売されましたが、実際に使ってみると時計以外の用途では天気予報の確認とメールやLINEなどの確認用といった用途でしか使ってませんでした。
そのため、バッテリーのことを考えると普通の腕時計でいいんじゃ・・・と感じてました。

そこで、Apple Watch のもっと役立つ使い方について考えたところ流行りの IoT との相性がもっともいいと思いました。
すでに、スマートフォンからエアコンなどの家電を制御している人はたくさんいますが、は常に身につけるものであるため、格段に便利になると思います。
今回は Apple Watch からエアコンを制御することにしました。

概要

f:id:inner2:20160110094620p:plain

Apple Watch でインターネットを通じて家電を制御するために、上の図のような形を考えました。
見ての通り、Apple Watch から iPhone を介して命令を Twitter でつぶやきます。
そして、自宅に設置した Raspberry PI で Twitter の Timeline を監視し、命令がきた時に Arduino に命令を Serial 通信で送ります。
Arduino では赤外線 LED でエアコンに信号を送ります。

Twitter を利用している部分は Django や Flask などのフレームワークを利用して WEB アプリを作りたかったのですが、 セキュリティの問題が不安だったため、Twitter を利用しました。
今回は、ソースコードがなるべくシンプルになるようにエアコンの「冷房・暖房・停止」だけでまとめますが、
照明をつけたりするのも少し変更するだけでできます。

環境

  • Raspberry PI1 Model B
    • OS: RASPBIAN JESSIE
    • Python version 3.4.3

【永久保証付き】Arduino Uno

【永久保証付き】Arduino Uno

Apple Watch から iPhone を介して Twitter でつぶやく

Apple Watch から直接 Twitter でつぶやくことはできないため、 Apple Watch から iPhone に命令を送り、iPhone のバックグラウンドでつぶやくようにします。
アプリの作成には主に下記のページを参考にしました。

Apple Watch アプリの画面
f:id:inner2:20160110094624p:plain

iPhone アプリの画面
f:id:inner2:20160110094621p:plain

作成手順

まずは、普通の iPhone アプリを作る。

  1. Xcode で新しいプロジェクトを作成。(template は iOS Application の Single View Application を選択)
  2. Main.storyboard を選択し、Button を追加する。

    • 今回はエアコンの「冷房(cooling)・暖房(heating)・停止(stop)」を制御するのでボタンは 3つ作成。
    • 同時にTitle で名前を変更した。

    f:id:inner2:20160110094622p:plain

  3. Button にクリックした時のイベントを追加する。

    f:id:inner2:20160110094623p:plain

  4. ViewController.swift を変更する。(code は長いので下に記載)

  5. ここからは、Apple Watch のアプリケーションを作成する。メニューバーの [Editor]-[Add Target...] を選択し、watchOS Application の WatchKit App を追加。
  6. interface.storyboard を選択し、Button を追加する。

    • 先ほどと同様に「冷房(cooling)・暖房(heating)・停止(stop)」の3つの Button 作成。

    f:id:inner2:20160110094625p:plain

  7. Button にクリックした時のイベントを追加する。

    f:id:inner2:20160110094626p:plain

  8. AppDelegate.swift と InterfaceController.swift を変更する。(code は下に記載)

ViewController.swift (code)

//
//  ViewController.swift
//  WatchApp
//
//  Created by inner on 2016/01/10.
//  Copyright © 2016年 inner. All rights reserved.
//

import UIKit
import Social
import Accounts

class ViewController: UIViewController {


    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        getTwitterAccountsFromDevice()
        selectTwitterAccount()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    // button click action iphone
    @IBAction func cooling_click(sender: AnyObject) {
        // tweet cooling
        postTweet("cooling")
    }
    @IBAction func heating_click(sender: AnyObject) {
        // tweet heating
        postTweet("heating")
    }
    @IBAction func stop_click(sender: AnyObject) {
        // tweet stop
        postTweet("stop")
    }

    // Twitter 関連
    // ------ Twitter Account -------
    let twitter_account: String = ""  // @ 以下のアカウント名を入力
    // ------------------------------


    var accountStore = ACAccountStore() //Twitter、Facebookなどの認証を行うクラス
    var twAccount: ACAccount? //Twitterのアカウントデータを格納する

    /* iPhoneに設定したTwitterアカウントの情報を取得する */
    func getTwitterAccountsFromDevice(){
        let accountType = accountStore.accountTypeWithAccountTypeIdentifier(ACAccountTypeIdentifierTwitter)
        accountStore.requestAccessToAccountsWithType(accountType, options: nil) { (granted:Bool, aError:NSError?) -> Void in

            // アカウント取得に失敗したとき
            if let error = aError {
                print("Error! - \(error)")
                return;
            }

            // アカウント情報へのアクセス権限がない時
            if !granted {
                print("Cannot access to account data")
                return;
            }
        }
    }
    // setting twitter account
    func selectTwitterAccount(){
        let accountType = accountStore.accountTypeWithAccountTypeIdentifier(ACAccountTypeIdentifierTwitter)
        let accounts = self.accountStore.accountsWithAccountType(accountType) as! [ACAccount]
        for account in accounts {
            if account.username == twitter_account{
                twAccount = account
            }
        }
    }
    // ツイートを投稿
    private func postTweet(cmd: String) {
        let URL = NSURL(string: "https://api.twitter.com/1.1/statuses/update.json")

        let now = NSDate()

        let formatter = NSDateFormatter()
        formatter.dateFormat = "yyyy/MM/dd HH:mm:ss"

        let date = formatter.stringFromDate(now)

        // ツイートしたい文章をセット
        let params = ["status" : cmd + "-" + date]

        // リクエストを生成
        let request = SLRequest(forServiceType: SLServiceTypeTwitter,
            requestMethod: .POST,
            URL: URL,
            parameters: params)

        // 取得したアカウントをセット
        request.account = twAccount

        // APIコールを実行
        request.performRequestWithHandler { (responseData, urlResponse, error) -> Void in

            if error != nil {
                print("error is \(error)")
            }
            else {
                // 結果の表示
                do {
                    let result = try NSJSONSerialization.JSONObjectWithData(responseData,
                        options: .AllowFragments) as! NSDictionary

                    print("result is \(result)")
                }
                catch {
                    return
                }
            }
        }
    }
}

AppDelegate.swift(code)

//
//  AppDelegate.swift
//  WatchApp
//
//  Created by inner on 2016/01/10.
//  Copyright © 2016年 inner. All rights reserved.
//

import UIKit
import WatchConnectivity

import Social
import Accounts

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, WCSessionDelegate {

    var window: UIWindow?
    let wcSession = WCSession.defaultSession()

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        // Override point for customization after application launch.
        if WCSession.isSupported() {
            wcSession.delegate = self
            wcSession.activateSession()
        }
        return true
    }

    // get message from watch
    func session(session: WCSession, didReceiveMessage message: [String: AnyObject], replyHandler: [String: AnyObject] -> Void) {

        // watchからのメッセージを受け取り
        if let watchMessage = message["toParent"] as? String {
            // twitter account の選択
            selectTwitterAccount()
            postTweet(watchMessage)
        }
        else{
            print("error: session receive")
        }
    }

    // Twitter関連
    // ------ Twitter Account -------
    let twitter_account: String = ""  // @ 以下のアカウント名を入力
    // ------------------------------
    var accountStore = ACAccountStore() //Twitter、Facebookなどの認証を行うクラス
    var twAccount: ACAccount? //Twitterのアカウントデータを格納する

    // setting twitter account
    func selectTwitterAccount(){
        let accountType = accountStore.accountTypeWithAccountTypeIdentifier(ACAccountTypeIdentifierTwitter)
        let accounts = self.accountStore.accountsWithAccountType(accountType) as! [ACAccount]
        for account in accounts {
            if account.username == twitter_account{
                twAccount = account
            }
        }
    }

    // ツイートを投稿
    private func postTweet(cmd: String) {
        let URL = NSURL(string: "https://api.twitter.com/1.1/statuses/update.json")

        let now = NSDate()

        let formatter = NSDateFormatter()
        formatter.dateFormat = "yyyy/MM/dd HH:mm:ss"

        let date = formatter.stringFromDate(now)

        // ツイートしたい文章をセット
        let params = ["status" : cmd + "-" + date]

        // リクエストを生成
        let request = SLRequest(forServiceType: SLServiceTypeTwitter,
            requestMethod: .POST,
            URL: URL,
            parameters: params)

        // 取得したアカウントをセット
        request.account = twAccount

        // APIコールを実行
        request.performRequestWithHandler { (responseData, urlResponse, error) -> Void in

            if error != nil {
                print("error is \(error)")
            }
            else {
                // 結果の表示
                do {
                    let result = try NSJSONSerialization.JSONObjectWithData(responseData,
                        options: .AllowFragments) as! NSDictionary

                    print("result is \(result)")
                }
                catch {
                    return
                }
            }
        }
    }


    func applicationWillResignActive(application: UIApplication) {
        // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
        // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
    }

    func applicationDidEnterBackground(application: UIApplication) {
        // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
        // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
    }

    func applicationWillEnterForeground(application: UIApplication) {
        // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
    }

    func applicationDidBecomeActive(application: UIApplication) {
        // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
    }

    func applicationWillTerminate(application: UIApplication) {
        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
    }


}

InterfaceController.swift

//
//  InterfaceController.swift
//  watch_iot Extension
//
//  Created by inner on 2016/01/10.
//  Copyright © 2016年 inner. All rights reserved.
//

import WatchKit
import Foundation
import WatchConnectivity

class InterfaceController: WKInterfaceController, WCSessionDelegate {

    let wcSession = WCSession.defaultSession()

    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)

        // Configure interface objects here.
        init_send()
    }

    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
    }

    override func didDeactivate() {
        // This method is called when watch view controller is no longer visible
        super.didDeactivate()
    }

    func init_send() {

        // Configure interface objects here.
        // WCSessionの開始
        if WCSession.isSupported() {
            wcSession.delegate = self
            wcSession.activateSession()
        }

        // reachableの確認
        if wcSession.reachable {
            print("reachable")
            // reachableであればメッセージを送る
            sendMessageToParent("init")
        }
        else{
            print("not reachable")
        }
    }

    // button click action
    @IBAction func cooling_click() {
        sendMessageToParent("cooling")
    }

    @IBAction func heating_click() {
        sendMessageToParent("heating")
    }

    @IBAction func stop_click() {
        sendMessageToParent("stop")
    }

    // send message
    func sendMessageToParent(msg:String){
        print("sendMessageToParent()")
        let message = [ "toParent" : msg ]
        wcSession.sendMessage(message, replyHandler: { replyDict in }, errorHandler: { error in })

    }

}

補足

  • iPhonetwitter を利用するときは[設定]-[Twitter]-[アカウントの使用を許可するAPP]で作成したプログラムにチェックが入っている必要があります。
  • Twitter は短時間に同じツイートをできないため、つぶやくときには現在の時間を入れています。

後編へ

長くなりすぎたため、前編と後編に分けました。
後編をどうぞ。

inner2.hatenablog.com