見出し画像

Jackeryポータブル電源、バッテリ残量監視プログラムを雑に説明

1. はじめに

前回の記事で紹介した仕組みを、かなり雑にまとめます。(というかコードベタ貼り)そのうち少しづつ手直ししてわかりやすくしようと思ってます。(たぶん。。。)


2. サンプルプログラム

こんなPythonのプログラムで情報が取得できます。
パケットキャプチャして取得できた情報を、そっくりそのまま送信できるようにChatGPT大先生にプログラムを書いてもらっただけです。

import requests
import json

# APIのURL 実際のディバイスIDに置き換えてください
url = 'https://iot.jackeryapp.com/v1/device/property?deviceId=XXXXXXXXXX'

# ヘッダ情報を辞書で定義
headers = {
    'content-type': 'application/json',
    'accept': '*/*',
    'app_version': '1.0.5',
    'sys_version': '17.2',
    'accept-encoding': 'br;q=1.0, gzip;q=0.9, deflate;q=0.8',
    'accept-language': 'ja-JP',
    'platform': '1',
    'token': 'XXXXXXXXXXX',  # 実際のトークンに置き換えてください
    'user-agent': 'DxPowerProject/1.0.5 (com.hb.jackery; build:2; iOS 17.2.0) Alamofire/5.8.0',
    'model': 'iPad Pro (12.9-inch) (3rd generation)'
}

# GETリクエストを送信
response = requests.get(url, headers=headers)

# レスポンスのステータスコードを確認
if response.status_code == 200:
    # レスポンスボディをJSON形式で取得
    data = response.json()
    # JSONデータを整形して表示
    pretty_json = json.dumps(data, indent=4, ensure_ascii=False)
    print(pretty_json)
else:
    print(f'Error: {response.status_code}')

3. レスポンス

こんな情報が返ってきます。

{
    "rsaForAesKey": null,
    "token": "",
    "code": 0,
    "msg": "SUCCESS",
    "data": {
        "device": {
            "id": "xxxxx",
            "modelId": "xxxxx",
            "modelCode": 5,
            "modelName": "HTE1031000A",
            "deviceCode": "xxxxx",
            "deviceName": "JE-1000C",
            ...
        },
        "properties": {
            "lm": 0,
            "ast": 1440,
            ...
            "rb": 77,
            ...
        }
    },
    "encryption": false
}

3.1 現状調べて読み取れた properties の項目リスト

レスポンス内容からおそらくこのような値ではないかと解析した結果です。わからない値も多いので、こんなことわかったよ! って情報があればコメントいただけると嬉しいです。

  • acip, AC電源入力(W)

  • acohz, AC電力出力ヘルツ数(50/60Hz)

  • acov, AC電力出力電圧(V)

  • acpsp, 入力電力(W)と思われる値、JE-1000Cのみ ip と同じような動きをする。JE-300Bでは0しか計測していない。

  • acpss, 不明、0 or 1 の値を取っており何かのスイッチ値と思われる

  • ast, 時間設定値(おそらくオートスリープ) 1440, 720, 0 などの値を取る

  • bs, 不明、今のところ常に0の値しか取得していない。

  • bt, おそらくbattery temperature (蓄電池温度) 220 などと出力され、22.0度という意味と思われる。

  • cs, 不明、0 or 1 の値を取っており何かのスイッチ値と思われる

  • ec, 不明、今のところ常に0の値しか取得していない。

  • ip, 入力電力(W)と思われる値

  • it, 充電完了時間 999だと 99.9h という意味

  • lm, ライトモードスイッチ 0/1 Off/On

  • lps, 不明、今のところ常に0の値しか取得していない。

  • oac, AC電源出力スイッチ 0/1 Off/On

  • op, AC電源出力電力(W)

  • ot, 出力可能時間 105 では 10.5h という意味

  • pal, 不明、今のところ常に0の値しか取得していない。

  • pm, 時間設定値(何のスリープ値かは未確認) 1440, 720, 0 などの値を取る

  • pmb, 不明、0 or 1 の値を取っており何かのスイッチ値と思われる

  • rb, バッテリー残量(%)

  • slt, 不明、今のところ常に0の値しか取得していない。

  • ta, 不明、今のところ常に0の値しか取得していない。

4. サンプルプログラムその2

データを取得してローカルファイルに保存しつつ、ambient.io へデータをアップするプログラムです。実際にこのプログラムを使って一定間隔実行して情報をアップロードしてます。

import requests
import time
from datetime import datetime
import json
import ambient

# ambient接続用
ambi1 = ambient.Ambient(xxxxx, "xxxxxxxxxxx")
ambi2 = ambient.Ambient(xxxxx, "xxxxxxxxxxx")

# TinyDB データベースの初期化
db = TinyDB('db.json')

# APIのURL
url1 = 'https://iot.jackeryapp.com/v1/device/property?deviceId=XXXXXXXXXXXXXX1'
url2 = 'https://iot.jackeryapp.com/v1/device/property?deviceId=XXXXXXXXXXXXXX2'

# ヘッダ情報を辞書で定義
headers = {
    'content-type': 'application/json',
    'accept': '*/*',
    'app_version': '1.0.5',
    'sys_version': '17.2',
    'accept-encoding': 'br;q=1.0, gzip;q=0.9, deflate;q=0.8',
    'accept-language': 'ja-JP',
    'platform': '1',
    'token': 'XXXXXXXXXXXX',  # 実際のトークンに置き換えてください
    'user-agent': 'DxPowerProject/1.0.5 (com.hb.jackery; build:2; iOS 17.2.0) Alamofire/5.8.0',
    'model': 'iPad Pro (12.9-inch) (3rd generation)'
}

def fetch_and_save_data(url, ambi):
    """APIからデータを取得し、TinyDBに保存する"""
    response = requests.get(url, headers=headers)
    if response.status_code == 200:
        data = response.json()
        # 取得日時の追加
        data['fetch_time'] = datetime.now().isoformat()
        # データをTinyDBに保存
        db.insert(data)
        print("Data saved to TinyDB")

        # JSONデータを整形して表示
        pretty_json = json.dumps(data, indent=4, ensure_ascii=False)
        print(pretty_json)

        # ambientにデータを送信
        properties = data['data']['properties']
        rb = properties.get('rb', 0)
        ip = properties.get('ip', 0)
        it = properties.get('it', 0) / 10  # it の値を1/10にする
        op = properties.get('op', 0)
        ot = properties.get('ot', 0) / 10  # ot の値を1/10にする
        acip = properties.get('acip', 0)
        bt = properties.get('bt', 0)
        acpsp = properties.get('acpsp', 0) / 10  # acpsp の値を1/10にする
        
        r = ambi.send({"d1": rb, "d2": ip, "d3": it, "d4": op, "d5": ot, "d6": acip, "d7": bt, "d8": acpsp})
        print("Data sent to Ambient")
    else:
        print(f'Error: {response.status_code}')

fetch_and_save_data(url1, ambi1)
time.sleep(10)
fetch_and_save_data(url2, ambi2)

5. deviceIdとtokenの入手方法

すみません。パケットキャプチャするという力技ぐらいでしか手に入らないです。もう少しスマートな手順が紹介できたらよかったんですが、認証をパスしてtokenを取得するプログラムがどうにも動作しなくて断念しました。

5.1 アプリファイルから取得する方法

もし、apple silicon の macOS (M1 macなど) を使っている方であれば、macOS上で jackeryのアプリを動かして認証すると、ローカルに tokenなどのファイルが保存されるのでそちらを見るという方法でも手に入ります。

% cd ~/Library/Containers/[App ID]/Data/Library/Caches/com.hb.jackery

% ls
Cache.db	Cache.db-shm	Cache.db-wal	fsCachedData

[App ID] は利用環境によって変わります。74DA0000-0123-4000-8DED-0B95926C1111 みたいな感じの文字列です。 ~/Library/Containers/ にあるどれかのディレクトリが Jackeryアプリなので頑張って探すしかないです。
コマンドプロンプトで見ると文字列の羅列なのですが、Finderで開くとアプリ名でディレクトリ名がなぜか表示されるのでその方法で私は探し当てました。

Cache.db は SQLite のデータベースなので、DB Browser for SQLite などで開くと中身が見れます。中にはサーバとのやりとりをした情報が保存されていて、その中にtokenなどの情報があります。 もしくは strings コマンドでCache.db-walなどから直接文字列だけ引き抜くという力技でもなんとかなります。

% strings Cache.db-wal

なんのことやらさっぱりだとは思いますが、わかりやすい手順にまとめられるほど知識も技術もなかったのですみません。

5.2 token取得用プログラムの残骸(動作しません)

認証サーバへユーザ名とパスワードを入れて tokenが取得できればそれが一番良いので、プログラムを作成して試してみたのですが動作せず断念しました。
私には知識が足りず達成できなかったですが、どなたかチャレンジされる方の為に役に立つ情報があるかもしれないので作ったコードの残骸を置いときます。

実行しても正常に動作しません
認証に失敗しているのかなんの返答も返ってきません。

from Crypto.Cipher import AES, PKCS1_OAEP
from Crypto.PublicKey import RSA
from Crypto.Random import get_random_bytes
import base64
import requests
import json
import urllib.parse

# 以下の変数を適切に設定してください
user_id = "YOUR_USER_ID"
password = "YOUR_PASSWORDK"
public_key_str = """-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCVmzgJy/4XolxPnkfu32YtJqYGFLYqf9/rnVgURJED+8J9J3Pccd6+9L97/+7COZE5OkejsgOkqeLNC9C3r5mhpE4zk/HStss7Q8/5DqkGD1annQ+eoICo3oi0dITZ0Qll56Dowb8lXi6WHViVDdih/oeUwVJY89uJNtTWrz7t7QIDAQAB
-----END PUBLIC KEY-----"""

# RSA公開鍵の読み込み
public_key = RSA.import_key(public_key_str)

# AESキーと暗号化オブジェクトの生成
aes_key = get_random_bytes(16)  # 16バイトのAESキー
cipher_aes = AES.new(aes_key, AES.MODE_EAX)
nonce = cipher_aes.nonce

# ユーザーIDとパスワードを結合して暗号化
data = f"{user_id}:{password}".encode()
ciphertext, tag = cipher_aes.encrypt_and_digest(data)

# AESキーをRSA公開鍵で暗号化
cipher_rsa = PKCS1_OAEP.new(public_key)
encrypted_aes_key = cipher_rsa.encrypt(aes_key)

# Base64エンコード
encoded_ciphertext = base64.b64encode(ciphertext).decode('utf-8')
encoded_encrypted_aes_key = base64.b64encode(encrypted_aes_key).decode('utf-8')

# リクエストデータの準備
base_url = "https://iot.jackeryapp.com/v1/auth/login"
params = {
    "aesEncryptData": encoded_ciphertext,
    "rsaForAesKey": encoded_encrypted_aes_key
}
encoded_params = urllib.parse.urlencode(params)
full_url = f"{base_url}?{encoded_params}"

# マルチパートフォームデータのボディを準備
files = {
    'file': ('', '', 'application/octet-stream'),
}
# カスタムヘッダーの設定(必要に応じて他のヘッダーも追加)
headers = {
    'Accept': '*/*',
    'Content-Type': 'multipart/form-data; boundary=alamofire.boundary.d421aee0ebae97e7',
    'app_version': '1.0.5',
    'sys_version': '17.2',
    'accept-language': 'ja-JP',
    'platform': '1',
    'User-Agent': 'DxPowerProject/1.0.5 (com.hb.jackery; build:2; iOS 17.2.0)',
}

# POSTリクエストを出力
print("------full_url------")
print(full_url)
print("------headers------")
print(headers)

# POSTリクエストの送信
response = requests.post(full_url, headers=headers, files=files)

# レスポンスの出力
print("------response------")
print(response.text)
print("--------------------")

どなたか知識のある方で改良点などお気づきでしたらコメントいただけると嬉しいです。
public_key_str にはこれが公開鍵ではないかと思われるものを指定していますが、これが正解なのかもわかってません。認証の方式もパケットキャプチャ結果から推察しているだけなので詳細は不明なためとりあえず、想像の範囲で ChatGPT大先生にお願いしてつくってもらいました。
そもそも私はWeb系エンジニアでないのでこの辺の知識がさっぱりでして、すごく単純なミスで動かないだけという可能性もあります。詳しい方のコメントお待ちしております。

6. おわりに

すごく雑なまとめかたでごめんなさい。
わかる人はぜひ活用してください。
また、試して色々わかったことがあればぜひ教えてください。

そのうちもう少し綺麗に書き直します。


この記事が気に入ったらサポートをしてみませんか?