Event

SwiftGarden 1.センサーデータを取得する – SwiftからPythonコードを呼び出す

1.センサーデータを取得する – SwiftからPythonコードを呼び出す

この記事はiOSDC2023で発表したセッション、Swiftでりんごを育ててみたの発表の第1部の内容をまとめた記事になります。
以下の図の①の実装についてのものです

「0.導入 – 発表やSwiftGardenの概要紹介 」の記事の続編です。もしまだ読んでいない方は読んでいただけると!

SwiftGarden 0.Introduction Announcement and overview of SwiftGardenThis article is an article that summarizes the contents of the introduction of "I tried to grow apples with Swift"!...

SwitchBotのセンサーからデータを取得する

今回センサーとして利用しているのは以下のSwitchBotの温湿度センサーです

SwitchBotのデータはAPI経由で取得できます(こちらも去年の発表の資料に使用しました)

SwitchBotのAPIを使って、SwitchBotデバイスを操作するSwitchBotのAPIを使って、SwitchBotデバイスを操作する方法をまとめてみました!...

ただし昨年の発表時と異なり、認証周りの仕組みがv1.1に変更され、API実行時に一意のsignatureを発行する必要があります(参考)。
それ以外に点については、上記の記事に記載の情報と同様です

今回はSwiftをLinux上で動かす想定で、CryptoKitは使用できないので、Linuxでも利用できるswift-cryptoを使用する想定でした。
上記のSwitchBotのサイトには、このsignatureを発行するサンプルが複数の言語で記載されていますが、Swiftを使って実装したサンプルは載っていませんでした。
swift-cryptoを使用したコードの実装例は以下のとおりです
Tokenとシークレットは、SwitchBotアプリから発行できます

import Crypto

static func generateAuthInfo() throws -> SwitchbotAuthInfo {
    let token = Secrets.Switchbot.token
    let secret = Secrets.Switchbot.clientSecret
    let timestamp = String(Int64(Date().timeIntervalSince1970 * 1000))
    let nonce = UUID().uuidString
    let data = token + timestamp + nonce

    let key = SymmetricKey(data: secret.data(using: .utf8)!)
    let hmac = HMAC<SHA256>.authenticationCode(for: data.data(using: .utf8)!, using: key)
    let sign = Data(hmac).base64EncodedString()

    return .init(nonce: nonce, timestamp: timestamp, sign: sign, token: token)
}

しかし今回は諸事情(別の優先度の高いasync-http-clientと一緒にビルド時にRaspberryPiでエラーが出たため)のため、このswift-cryptoを使用したSwiftでの発行ではなく、Pythonを後述のPythonを利用した方法でsignatureを発行します(他の方法を使えばSwiftでも発行できそうでしたが、セキュリティ的に危なそうだったので、断念しました)

PythonのコードをSwiftから呼び出す

昨年の発表でも使用したPythonKitを使ったSwiftからのPythonの呼び出し処理です

4. カードリーダーと連携する - SuicaのIDを読み取るこの記事は、「Swiftで我が家をより便利に、安全に」の発表の4部「4. カードリーダーと連携する - SuicaのIDを読み取る 」の内容をまとめた記事になります!...

昨年の実装と異なる点は、①Macでの実行時にpyenvでバージョンを管理したPythonを利用する、②Pythonで処理した結果を戻り値として返却し、Swift側で利用する、という点です

まずpyenvを使用した場合の実装ですが、そのままだとPythonのコマンド側を読み込めないので、どのバージョンのライブラリを利用するかを指定する必要があります
以下はMacの場合は3.10.6を利用、ラズパイの場合は3.9.0を利用するように指定した場合のサンプルコードです

        let pythonPath: String
#if os(Linux)
        pythonPath = "/lib/arm-linux-gnueabihf/libpython3.9.so"
#else
        pythonPath = "/Users/${USER}/.pyenv/versions/3.10.6/lib/libpython3.10.dylib"
#endif
        PythonKit.PythonLibrary.useLibrary(at: pythonPath)
        do {
            try PythonLibrary.loadLibrary()
        } catch {
            print(error)
        }

そしてSwitchBotのページに記載のあるサンプルコードを参考に、以下のようなコードを用意します

import json
import time
import hashlib
import hmac
import base64
import uuid
import sys

def generate_sign(token, secret):
    nonce = str(uuid.uuid4())
    t = str(int(round(time.time() * 1000)))
    string_to_sign = '{}{}{}'.format(token, t, nonce)
    string_to_sign = bytes(string_to_sign, 'utf-8')
    secret = bytes(secret, 'utf-8')
    sign = base64.b64encode(hmac.new(secret, msg=string_to_sign, digestmod=hashlib.sha256).digest())
    data = {
        "token": token,
        "timestamp": t,
        "sign": str(sign, 'utf-8'),
        "nonce": nonce
    }
    return json.dumps(data) // return string json-formatted

if __name__ == "__main__":
    token = sys.argv
    secret = sys.argv
    result = generate_sign(token, secret)

Package.swiftに上記のgenerate_sign.pyをresourceとして登録し、以下のような形式で上記のコードを呼び出し、受け取るJSON形式の文字列をDecodablestructに変換することで、Swift側のコードでAPI実行に必要な要素を用意できます

struct SwitchbotAuthInfo: Decodable {
    let nonce: String
    let timestamp: String
    let sign: String
    let token: String
}

static func generateAuthInfo() throws -> SwitchbotAuthInfo {
    let jsonString = try PythonCall.generateSwitchbotSign()
    let jsonData = jsonString.data(using: .utf8)!
    let info = try JSONDecoder().decode(SwitchbotAuthInfo.self, from: jsonData)
    return info
}

あとは以下のような形式でheaderに必要な情報をセットして、APIを実行すれば、OKです!(以下はAlamofireを使った実装です)

let authInfo = try generateAuthInfo()
let headers: Alamofire.HTTPHeaders = [
    .authorization(authInfo.token),
    .init(name: "sign", value: authInfo.sign),
    .init(name: "t", value: String(authInfo.timestamp)),
    .init(name: "nonce", value: authInfo.nonce)
]

これで成功すると、以下のようなJSONを取得できます!(一部の値は省略しています)

{
    "message": "success",
    "body": {
        "humidity": 79,
        "deviceType": "Meter",
        "temperature": 24.1,
        "version": "V2.5",
        "battery": 87
    }
}

これで必要な温度と湿度のデータを取得できました!

まとめ

この記事では、SwiftGardenでSwitchBotのセンサーのデータをPythonとSwiftを組み合わせて取得する方法をまとめました
次の記事は「2. ラズパイでFirebaseにデータを送る – SwiftのCLIツールを動かす」です!

SwiftGarden 2.ラズパイでFirebaseにデータを送る - SwiftのCLIツールを動かすこの記事は、「Swiftでりんごを育ててみた」の2部「ラズパイでFirebaseにデータを送る - SwiftのCLIツールを動かす」の内容をまとめた記事になります!...
+1

COMMENT

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA