SwitchBotの防水温湿度計のデータをS3に保存(その3)

前回の続き。

センサーの値をWebhookで通知してもらえるようになったので、Webhookの受け口を作ります。SwitchBotのAPIは下を参照。

GitHub - OpenWonderLabs/SwitchBotAPI: SwitchBot Open API Documents

上のSwitchBotのWebhookによってセンサーの情報がjsonでPOSTされてくるので、それの受け口をAWSAPI GatewayとLambdaで作り、POSTされたjsonをそのままS3に保存します。

Lambda関数内はPythonで書いてこんな感じ。

import  json
import boto3
import datetime

S3 = boto3.resource('s3')
BUCKET_NAME = "バケット名"
BUCKET = S3.Bucket(BUCKET_NAME)
PATH = "webhook/"

# AWS API Gatewayからこの関数が呼び出される。
# API Gatewayの設定で「Lambdaプロキシ統合しない」に設定しておくと、
# 引数eventの中身がWebhookのbodyで来たjsonそのものが渡されてくる。

def lambda_handler(event, context):
    json_data = event
    # print(json_data)

    time_of_sample = json_data["context"]["timeOfSample"]
    dt = datetime.datetime.fromtimestamp(time_of_sample/1000)
    object_key_name = PATH + f"{dt.year:02d}/{dt.month:02d}/{dt.day:02d}/{time_of_sample}.json"

    BUCKET.put_object(
        Key=object_key_name,
        Body=json.dumps(json_data, ensure_ascii=False)
        )

    return {
        'statusCode': 200,
        'body': json_data
    }

WebhookからPOSTされる中身はこんな感じ。

{
    "eventType": "changeReport",
    "eventVersion": "1",
    "context": {
        "deviceType": "WoIOSensor",
        "deviceMac": "MACアドレス",
        "temperature": 10.2,
        "humidity": 76,
        "battery": 100,
        "scale": "CELSIUS",
        "timeOfSample": 1701820882473
    }
}

WebhookでPOSTされるjson内の"timeOfSample"にepoch時間が入っているので、AWS Lambda関数内でjson内の"timeOfSample"を見て年/月/日のパスに分けてjsonファイルとしてS3に保存しています。

動かしてみると...できた! SwitchBotのWebhookはちゃんとセンサーの値に変化があった時にPOSTされてきます。 めでたしめでたし。

SwitchBotの防水温湿度計のデータをS3に保存(その2)

前回の続き。

SwitchBotのAPIを調べていたら、なんとWebhookでセンサーの値を任意のWebに通知できるようになっているではないですか。

GitHub - OpenWonderLabs/SwitchBotAPI: SwitchBot Open API Documents

Webhookで通知してもらうにはセンサー単独でなくSwitchBotの"ハブ"

が必要とのことでこちらを入手。

SwitchBot ハブミニwww.switchbot.jp

ハブミニと防水温湿度計をSwitchBotアプリで連携とクラウドサービスをオンに。

そしたらセンサーの値を通知してもらうためのWebhook受け口を何らか自分で用意して、そこに通知してもらうようにSwitchBotAPIに登録すればいい。

Webhookの受け口はテスト用に webhook.site を利用しました。便利だ。

上のSwitchBotAPIドキュメントを見ながら、ざっくりこんな感じ。コード継はぎで整ってないのはご容赦を。

# Declare empty header dictionary
apiHeader = {}
# open token
access_token =  "アクセストークン(これについては上のドキュメントに書いてある)"
# secret key
secret_key = "シークレットキー(これについても上のドキュメントに書いてある)"
nonce = uuid.uuid4()
t = int(round(time.time() * 1000))
string_to_sign = "{}{}{}".format(access_token, t, nonce)

string_to_sign = bytes(string_to_sign, "utf-8")
secret_key = bytes(secret_key, "utf-8")

sign = base64.b64encode(
    hmac.new(secret_key, msg=string_to_sign, digestmod=hashlib.sha256).digest()
)
print("Authorization: {}".format(access_token))
print("t: {}".format(t))
print("sign: {}".format(str(sign, "utf-8")))
print("nonce: {}".format(nonce))

# Build api header JSON
apiHeader["Authorization"] = access_token
apiHeader["Content-Type"] = "application/json"
apiHeader["charset"] = "utf8"
apiHeader["t"] = str(t)
apiHeader["sign"] = str(sign, "utf-8")
apiHeader["nonce"] = str(nonce)

######################
# Webhook登録

WEBHOOK_URL = "自分で用意した通知してほしい先のURL"

# Request body
req_body_json = {
    "action": "setupWebhook",
    "url": WEBHOOK_URL,
    "deviceList": "ALL",
}
# リクエストを送信
response = requests.post(
    "https://api.switch-bot.com/v1.1/webhook/setupWebhook",
    json=req_body_json,
    headers=apiHeader,
)

# レスポンスを確認
if response.status_code != 200:
    raise ValueError(f"Status code: {response.status_code}")

data = response.json()
print(f"レスポンス:{data}")

そしたら、webhook.site に用意したURLにjsonでセンサーの値がちゃんと通知されました。こんな感じ。

{
    "eventType": "changeReport",
    "eventVersion": "1",
    "context": {
        "deviceType": "WoIOSensor",
        "deviceMac": "MACアドレス",
        "temperature": 10.2,
        "humidity": 76,
        "battery": 100,
        "scale": "CELSIUS",
        "timeOfSample": 1701820882473
    }
}

素晴らしい!やりたいことの半分はこれでできてしまった。あとはWebhookをAWSで受けてjsonをS3に保存するところです。 その続きはまた今度。

SwitchBotの防水温湿度計のデータをS3に保存(その1)

SwitchBotの防水温湿度計のデータをS3に保存したくなった。

ちょっと調べたらラズパイからBluetooth使って取れそうだ。

ネット上のほとんど記事などは「防水温湿度計」じゃなくて、こちらの温湿度計の場合について書いてある。

しかし、防水温度計(Outdoor Temperature/Humidity Sensor)の場合は、[温湿度計]の場合とはコードをちょっと変えないとセンサーからのデータを正しく読めない。下参照。 github.com しかも私の環境では上ドキュメントにあるOutdoor Temperature/Humidity Sensorのバッテリーのデータを取るコードのところで、

引用

# Data from Type: 0x16 (Service Data)
battery_pct = data[5]

上の部分、私の環境ではdata[4]の位置にバッテリー残量のデータが入ってきてた。なんだろ...。

と、ここまでやってようやくセンサーの情報を取れるようになったものの、実はBluetoothでセンサーの情報を取ったりする必要ないことに後で気づいて、結局このへんのBluetoothでセンサーデータ読むコードやラズパイも使わないことになったのでした。その続きはこちら

M2来りて口笛を吹く


とうとう私のところにもAppleシリコンがやって来た! MacBook Air 15 inch M2だ! これまでうちのMacIntel機だったのでこれが初Appleシリコンだ! 諸般の事情により先代のProからAirへのグレードダウンだが、うれしいので気にならないぞ!
先代のMacBook Proの時 MacBook Pro 16インチモデル 来た! - osamuk's blog と同様、移行アシスタントは使わず手作業で育てている途中。まだしばらくかかりそうだけど、それもいいのだ。

Polars 0.19.2 で便利そうな変更があった「Add syntactic sugar for col("foo") -> col.foo」

Polarsのバージョン polars-0.19.2 で、こんなのが来た!

github.com

この書き方は便利そうだぞ、早速使おう。列名が日本語も大丈夫だった。(列名にPythonで変数名に使えない記号があるとダメだ、それはそうだよね。)

Polars 200本ノック

Polarsを使い始めたので、

Pandas2.0に移行しようと思ったけど、結局Polarsに移行した。 - osamuk's blog

使い方など調べていたら"100本ノック"系が2つありました。

qiita.com

qiita.com

それらあわせて200本ノックやってみました。わからない所はすぐにカンニングしつつ自分好みの書き方も考えつつやったのですが、なかなか疲れました。コード整える気力は残らなかったです。

Polarsはまだ更新が頻繁で引数名とか変わったりしているので、他の方がちょっと前に書いたコードは動かなかったりします。あと、機能追加で以前より簡単に書けるようになったりもしてます。当面は随時最新の情報を確認して使うのが良さそうです。

やった結果はこちら。(Polarsバージョン0.18.12で実行してます)

  • Polarsデータサイエンス100本ノック(構造化データ加工編)

Polars データサイエンス100本ノック(構造化データ加工編) · GitHub

  • Python初学者のためのPolars100本ノック

(こちらは転載不可でしたので、解答コードのみです)

初学者向けPolars100本ノック · GitHub

.NET 8 の新機能「時間抽象化」うれしいけど微妙

「時間抽象化」ってパッと見なんのことかわかりませんでしたが、DateTime.nowとか時関係の実装のテストで困るやつの対応なのですね。おおっ、それはうれしい。

「時間抽象化」

www.infoq.com

まだ公式ドキュメントには使用例とかがほとんど無くてちょっとわからないのですが、どうやら使うには結局新しい"TimeProvider"クラスを使うように既存の実装を書き換えねばならないみたい。時間係の実装のUnitTest対策としては正攻法でしょうが、MSがやるなら一歩突っ込んだ実装も期待したかったです。