Welcome to telecotele.com » Projects

Cube J1のAPI調査

NextDrive Cube J1からどうにかこうにかセンサデータを取得した記録です。

tags: cube house

Cube J1のAPI調査のカバー画像

はじめに

丁寧なす暮らしでGrafanaを復活させたあと、手当たり次第にメトリクスを収集したくなってきました。 自宅には以前から「NextDrive Cube J1」という製品があり、これに目を付けてみます。

NextDrive Cube J1とはHEMSゲートウェイのひとつであり、スマートメーターと接続して計測データを得ることが可能です。 また、USBやBluetoothにより環境センサなどの機器を接続しデータを得ることも出来ます。 以前はこのデバイスを使って「ネコリコホームプラス」というサービスが提供されていましたが、 残念ながら2023年3月29日をもってサ終となりました。

デバイスやアプリ (Ecogenie) はまだ使えているようですが、すでにサポート自体は打ち切られています。 Cube J1Cube J1に(現在は有償で?)アップグレードしてもらうと新アプリ (Ecogenie+) が使えるようになるものの、Cube J1時代では使えていた環境センサが使えなくなることと単純に面倒だな……ということで放置していました。

でもGrafanaも復活させたしということで触りだしてみます。 最終的な目標はCube J1からセンサデータを取得してGrafanaで可視化すること、ですがそもそもCube J1ではセンサデータ取得用のAPIなどは公開されていません。 それでもアプリやファームウェアを調査したところ、文書化されていない方法によりセンサデータ取得が可能なことがわかりました。 本稿ではその調査過程と、具体的なセンサデータ取得の方法について述べます。

センサデータ取得の方法

過程は良いから早くセンサデータ取得の方法が知りたい? しょうがないにゃあ・・先に具体的な方法から載せます。

まず、Cube J1の内部ではセキュアMQTTのサービスが動いており外部にポートも開いています。 ここにユーザ名とパスワードを用いて接続し、トピックをSubscribeするとデータが送られてきます。 ユーザ名はNULL以外であれば(おそらく)何でも良く、パスワードは後述するauth_tokenの値です。 auth_tokenを得る手段はいくつかあると思いますが、少なくともEcogenieアプリとCube J1とのHTTPS通信を観測することにより得られます。

これを基に、Pythonとpaho-mqttライブラリを用いてCube J1からセンサデータを取得するサンプルコードは次のとおりです。

import paho.mqtt.client as mqtt
import json

def on_omron(iid, payload):
    characteristics = {
        'services': 'services',                # characteristicsの情報
        '4009885713': 'Temperature',           # 温度
        '4009885712': 'Relative humidity',     # 湿度
        '4009950736': 'Ambient Light',         # 照度
        '4009942547': 'Barometric pressure',   # 気圧 
        '4009950738': 'Sound Noise',           # 騒音
        '4009950741': 'eTVOC',                 # 総揮発性有機化合物(TVOC)濃度相当値
        '4009950742': 'eCO2',                  # CO2濃度(eTVOC値から算出)
        '4009950739': 'Discomfort index',      # 不快指数
        '4009950740': 'Heat stroke',           # 熱中症警戒度
        '4009950743': 'Vibration information', # 振動情報
        '4009950744': 'SI value',              # SI値(地震動の破壊エネルギーの大きさ)
        '4009950753': '2JCIE-BU01 error status',
        '4009950752': '2JCIE-BU01 Latest sensing data',
        '4009950754': '2JCIE-BU01 Latest calculation data',
    }
    if iid in characteristics:
        print('on_omron', iid, characteristics[iid], json.loads(payload.decode().strip('\x00')))
    else:
        print('on_omron', iid, "UNKNOWN", payload)

def on_meter(iid, payload):
    characteristics = {
        'services': 'services',                                               # characteristicsの情報
        '42502272': 'public.nap.characteristic.wisun.operation-status',       # 動作状態 (Boolean)
        '42502355': 'public.nap.characteristic.smart-meter.coefficient',      # 積算値の換算係数
        '42502359': 'public.nap.characteristic.smart-meter.number',           # 積算電力量有効桁数
        '42502369': 'public.nap.characteristic.smart-meter.unit',             # 積算電力量単位
        '42502368': 'public.nap.characteristic.smart-meter.cumulative-amount-electric-energy-n', # 正方向(買電)積算電力量
        '42502371': 'public.nap.characteristic.smart-meter.cumulative-amount-electric-energy-r', # 逆方向(売電)積算電力量
        '42502375': 'public.nap.characteristic.smart-meter.instantaneous-electric-energy',       # 瞬時電力
        '42502376': 'public.nap.characteristic.smart-meter.instantaneous-electric-currents-r',   # R相瞬時電流
        '42502392': 'public.nap.characteristic.smart-meter.instantaneous-electric-currents-t',   # T相瞬時電流
        '42502387': 'public.nap.characteristic.smart-meter.rssi',             # 実際は"LQI"
        '42502384': 'public.nap.characteristic.smart-meter.b-route-id',       # b-route-id
        '42502385': 'public.nap.characteristic.smart-meter.b-route-password', # b-route-password
    }
    if iid in characteristics:
        print('on_meter', iid, characteristics[iid], json.loads(payload.decode().strip('\x00')))
    else:
        print('on_meter', iid, "UNKNOWN", payload)

def on_connect(client, userdata, flags, reason_code, properties):
    client.subscribe("#")
    #client.subscribe("$SYS/#")

def on_message(client, userdata, message):
    omron_uuid = '00000000-00000000-00000000-00000000' # YOUR DEVICE 
    meter_uuid = '00000000-00000000-00000000-00000000' #           UUID HERE

    token = message.topic.split('/', 1)
    device_uuid = token[0]
    if device_uuid == omron_uuid:
        on_omron(token[1], message.payload)
    elif device_uuid == meter_uuid:
        on_meter(token[1], message.payload)
    else:
        print('unknown topic', message.topic, message.payload)

if __name__ == '__main__':
    cubej1_ipaddr   = '192.168.xx.xx' # YOUR DEVICE_ADDR HERE
    cubej1_username = 'user'
    cubej1_password = 'xxxx'          # YOUR AUTH_TOKEN HERE

    mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
    mqttc.username_pw_set(cubej1_ipaddr, cubej1_password)
    mqttc.tls_set(cert_reqs=mqtt.ssl.CERT_NONE)
    mqttc.on_connect = on_connect
    mqttc.on_message = on_message
    mqttc.connect(cubej1_ipaddr, 8883)
    mqttc.loop_forever()

ここではCube J1がスマートメーターに接続され、USB接続タイプのオムロン環境センサが差さった環境を想定しています。 この2つにはそれぞれUUIDが振られており、おそらく各環境によって変わると思われます。 このコードでは送られてきたデータをただprintしているだけですが、ここまで来れば必要に応じて整形したりInfluxDBに投げる(そしてGrafanaで可視化する)なども可能でしょう。

Cube J1の調査

順番が前後しましたが、行った調査について述べていきます。 ちなみにこの調査については先行事例23ありです。

ポートスキャン

まずポートスキャンからはじめます。 TCP SYNスキャンの結果は次のとおりです。

PORT     STATE SERVICE
53/tcp   open  domain
80/tcp   open  http
443/tcp  open  https
8883/tcp open  secure-mqtt

53/tcpについては得られるものが無いと考えて未調査です。 80,443/tcpについては後述しますがnginxが動いています。 8883/tcpはMQTTについて見慣れておらず当初は独自サービスか何かだろうと思いスルーしてしまいましたが、 先行事例のツイートにより本当にMQTTっぽいことから調査を進めました。 最終的にこのMQTTが一番使い勝手が良いことが判明し、きっかけをくれたツイートに感謝します。

通信内容の調査

Fiddlerやmitmproxy、Wiresharkを使って通信内容を調査します。

Ecogineアプリ

Ecogineアプリの通信内容を見てみます。 このアプリではインターネットの先にあるNextDriverのサーバに加えて、 ローカルの同じブロードキャストドメインにいるCube J1とHTTPSで通信していました。

リクエストで使われるURLの例は次のとおりです。

https://192.168.xx.xx/nextdrive.api?command_type=info&sub_command_type=nextdrive&auth_token=xxxx

パスは/nextdrive.apiで、クエリパラメータにはcommand_typesub_command_typeauth_tokenが付いています。 command_typesub_command_typeによっては他にパラメータが増えたり減ったりします。 auth_tokenも付いたり付かなかったりしますが、これはMQTTでのアクセスにも必要なため記録しておくことをおすすめします。 auth_tokenが更新されることがあるかは不明ですが、少なくとも数日確認した限りでは変わらないようでした。 確認していないため推測ですが、機器の初回セットアップ時に決定されてその後は変化しないという感じだと嬉しいですが……。

なお、このURLに対するリクエストでは状況によってGETかPOSTが使われます。 またアプリ側ではUser-Agentも含めて送信していますが(少なくとも試した範囲では)別に無くても良く、さらにHTTPでアクセスしても同じレスポンスが返るようです。

Cube J1

Cube J1本体に出入りする通信も見てみます。 ただ、ほとんどの通信はTLSによって暗号化されており、さらに正しく証明書の検証もされているためmitmproxyを使っても内容は把握できませんでした。

DNSのリクエストとレスポンスを見ると、少なくともNextDriveと「IIJ IoTサービス」と思われるところには通信していそうです。 また、ローカルのブロードキャスト宛に54321/udpでLINKNEXTSRVCDSCYと書かれたパケット(次図)が2秒間隔で送られてきます。

"LINKNEXTSRVCDSCY"のパケット “LINKNEXTSRVCDSCY”のパケット

中身は「デバイス名」「ファームウェアバージョン」「UUID」「自身のIPアドレス」と思われる文字列であり、名前的にもService Discoveryの一種でしょう。 このパケットを見ればCube J1のIPアドレスが分かりますね(Ecogineアプリからも参照できます)

この他、たまにECHONET Liteのノード探索用パケットが飛びます。 冒頭での「Cube J1はHEMSゲートウェイ」という話の関連で、ECHONET Liteのノードが居ないか確認しているようです。 なお、もしかしてCube J1のデータもECHONET Liteから取れるのでは!?!?と試してみましたが、Cube J1のクラスグループは「コントローラ」でありセンサデータは渡してくれませんでした。

アプリの調査

Ecogineアプリも調査します。

ここではAndroid版アプリをデコンパイルするのみ……なので詳細は割愛しますが、 /nextdrive.apiのパラメータやMQTTから得られたデータの意味を把握することに役立ちしました。

また、詳細は確認していませんがauth_tokenはローカルのSQLiteか何かに保存されているようであり、 権限次第でアクセスできるようならアプリの通信内容を確認せずにauth_tokenを取得できる可能性があります。

ファームウェアの調査

Ecogineアプリの通信を見ていると、ファームウェアをダウンロードするためのURL4を見つけました。 そのURLからファームウェアをダウンロードし、binwalkで確認5します。

確認したところ、80,443/tcpで提供される/nextdrive.apiはnginxの静的モジュール (--with-http_nextdrive_module) として実装されていることが分かりました。 また、8883/tcpで提供されるセキュアMQTTのサービスはmosquittoが使われており、設定により「匿名接続は不可」「でもクライアント証明書は不要」「認証はlibnextdrive_mqtt_auth.soというauth_pluginで行う」とされていました。

これ以上はnginxやmosquitto auth_pluginのバイナリを確認する必要があります。 ということで、次からはこの確認の記録です。

nginx

この実体はARMv7向けにビルドされたELFのバイナリです。 Ghidraを用いて調査したところ、確かに/nextdrive.apiの処理を行っているようですが……クエリパラメータのパターンが多く追いきれませんでした。

ファイル読み書きできそうな雰囲気のcommand_type,sub_command_typeもありましたがそれは追えず。 なんとか追えたパラメータはすでにほぼアプリの通信調査により判明しており、新規で見つけたものも内蔵ストレージの使用量を取得するものでこれを知っても特に……というものでした。

mosquitto auth_plugin

この実体もARMv7向けにビルドされたELFのバイナリです。 mosquitto_plugin.hに従って書かれているので確認すると、mosquitto_plugin_version3を返してきました。 ということで、下記MOSQ_AUTH_PLUGIN_VERSION 3のインターフェースと突き合わせて確認していきます。

mosquitto/src/mosquitto_plugin.h at ce31269e050d3c727506bab4eb4562eedcfdab02 · eclipse/mosquitto

その結果、このpluginではmosquitto_auth_unpwd_checkにより認証が行われることが分かりました。 これはユーザ名とパスワードによる認証で使われる関数です。 さらに調査を進めると「ユーザ名およびパスワードがNULL以外」かつ「パスワードはauth_tokenと同じ値」を満たせば認証成功とされることが判明しました。

分解

まだファームウェアのダウンロードが出来ていなかったときに、最終手段として内蔵ストレージからファームウェアを取得しようと考えて一応分解していました(次図)。

Cube J1内部 Cube J1内部

怪しげなスルーホールも未調査です。 この裏にeMMCが付いています。(裏には2つ。あと表はラベルに隠れて見えませんがこれも?いま思うとファームウェア調査の際に/data/userdataというパスを見つけて、たとえばデバイスを初期化して中古で売る際にこれが本当に消えているか気になるような気にならなような……です)

調査結果

これまでの調査によって判明した、/nextdrive.apiとMQTTから得られるデータについて述べます。

/nextdrive.api

まずは/nextdrive.apiについてです。

ただ、後述するMQTTのほうが使い勝手が良く、気持ちがすでにそちらに向いています。 そのため/nextdrive.apiについては基本GET系で、動作確認しやすいもののみ調査しました。 この他にもfirmware_updaterebootresetuploadなどのcommandもありそうな気はしましたが動作確認はしていません。

システム操作系

↓ファームウェアの情報を返す /nextdrive.api?command_type=info&sub_command_type=nextdrive&auth_token=${AUTH_TOKEN}

<?xml version="1.0" encoding="utf-8"?>
<nextdrive-info api="." software="3.4.1028" build="Tue Dec 20 01:06:46 2022" update="3.4.1028" />

↓内蔵ストレージの情報を返す /nextdrive.api?command_type=info&sub_command_type=storage&auth_token=${AUTH_TOKEN}

<?xml version="1.0" encoding="utf-8"?>
<storage-info>
<storage name="userdata" type="hdd" fstype="ext4" size="13221158912" free="13132513280" label="" uuid="" timestamp="0" mode="rw"/>
</storage-info>

↓デバイス名を返す /nextdrive.api?command_type=system&sub_command_type=device_name&auth_token=${AUTH_TOKEN}

<?xml version="1.0" encoding="utf-8"?>
<device-name value="CubeJ-XXXX"/>

↓Wi-Fiの接続状況を返す /nextdrive.api?command_type=wifi&sub_command_type=get_status&auth_token=${AUTH_TOKEN}

<?xml version="1.0" encoding="utf-8"?>
<status id="0" wpa_state="COMPLETED" ssid="XXXXXXXX" bssid="xx:xx:xx:xx:xx:xx" address="xx:xx:xx:xx:xx:xx" ip_address="192.168.xx.xx"/>

↓周囲のSSIDを返す(sub_command_type=do_scanも存在しおそらくその結果と思われる。ステルスSSIDも返される) /nextdrive.api?command_type=wifi&sub_command_type=get_scan_result&auth_token=${AUTH_TOKEN}

<?xml version="1.0" encoding="utf-8"?>
<scan_result>
<ap bssid="xx:xx:xx:xx:xx:xx" freq="xxxx" rssi="xx" flag="[XXX]" ssid="XXXXXXXX"/>
<ap bssid="xx:xx:xx:xx:xx:xx" freq="xxxx" rssi="xx" flag="[XXX]" ssid="XXXXXXXX"/>
<ap bssid="xx:xx:xx:xx:xx:xx" freq="xxxx" rssi="xx" flag="[XXX]" ssid="XXXXXXXX"/>
<ap bssid="xx:xx:xx:xx:xx:xx" freq="xxxx" rssi="xx" flag="[XXX]" ssid="XXXXXXXX"/>
</scan_result>

↓謎(空JSONが返る) /nextdrive.api?command_type=wifi&sub_command_type=get_status&auth_token=${AUTH_TOKEN}

{}

↓管理者Role確認 /nextdrive.api?command_type=security&sub_command_type=authenticate&password=${ADMIN_PASSWORD}

<?xml version="1.0" encoding="utf-8"?>
<authentication role="admin"/>

センサデータ取得系

POSTでリクエストを送るようです。 すべては調べていませんが一例としてはこんな感じでした。

↓環境センサ情報のリクエスト(蓄積されたデータが返ってくる)

POST /nextdrive.api?command_type=accessory&sub_command_type=accessory_event&auth_token=${AUTH_TOKEN} HTTP/1.1
Host: 192.168.xx.xx
Content-Type: application/x-www-form-urlencoded

{"time_end":1707371230466,"char_type":"fe15","accessory_id":"xxxx","interval":3600000,"method":"aggregate","srv_type":"ef02","time_start":1707288430466}

↓電気料金の取得リクエスト(単価とともに送信すると蓄積データに基づいて計算された結果が返る)

POST /nextdrive.api?command_type=accessory&sub_command_type=electricity_fee&auth_token=${AUTH_TOKEN} HTTP/1.1
Host: 192.168.xx.xx
Content-Type: application/x-www-form-urlencoded

{"char_type":"88e3","elePlan":{"discount":null,"charge":[{"periods":[{"repeat":3600000,"condition":"inside","end":1707321600000,"count":24,"start":1707318000000}],"fees":[{"lower":300,"price":29.93,"upper":-1,"unit":"perkWh"},{"lower":120,"price":25.91,"upper":300,"unit":"perkWh"},{"lower":0,"upper":120,"price":19.43,"unit":"perkWh"}]}],"timeIntervalUnit":"hour","zeroBasicFee":230.86000000000001,"billingDay":[1698764400000,1701356400000,1704034800000,1706713200000,1709218800000,1711897200000,1714489200000,1717167600000,1719759600000,1722438000000,1725116400000,1727708400000,1730386800000,1732978800000,1735657200000],"normalBasicFee":1123.2},"accessory_id":"xxxx","srv_type":"0288","method":"get"}

MQTT

MQTTに接続すると${CUBE_J1のUUID}/accessoriesのトピックでCube J1本体と周辺機器の情報が書かれたJSONが送られてきます。 長いので折りたたみますが、

こんな感じです(一部編集)
{
  "accessories": [
    {
      "connected": true,
      "uuid": "${CUBE_J1のUUID}",
      "timezone": "Asia/Tokyo",
      "services": [
        {
          "type": "3E",
          "iid": 4063252,
          "characteristics": [
            {
              "type": "23",
              "value": "CubeJ-XXXX",
              "perms": [
                "pr"
              ]
            },
            {
              "type": "20",
              "value": "NextDrive Inc.",
              "perms": [
                "pr"
              ]
            },
            {
              "type": "30",
              "value": "ND12345678",
              "perms": [
                "pr"
              ]
            },
            {
              "type": "21",
              "value": "Cube-J1",
              "perms": [
                "pr"
              ]
            },
            {
              "type": "14",
              "value": null,
              "perms": [
                "pw"
              ]
            }
          ]
        }
      ]
    },
    {
      "uuid": "スマートメーターのUUID",
      "connected": true,
      "services": [
        {
          "type": "3e",
          "iid": 4063232,
          "characteristics": [
            {
              "type": "20",
              "iid": 4063264,
              "value": "NextDrive Inc."
            },
            {
              "type": "30",
              "iid": 4063280,
              "value": "SM12345678"
            },
            {
              "type": "21",
              "iid": 4063265,
              "value": "SmartMeter"
            },
            {
              "type": "23",
              "iid": 4063267,
              "value": "SM-XXXX"
            }
          ]
        }
      ]
    },
    {
      "uuid": "環境センサのUUID",
      "connected": true,
      "services": [
        {
          "type": "3e",
          "iid": 4063232,
          "characteristics": [
            {
              "type": "20",
              "iid": 4063264,
              "value": "OMRON"
            },
            {
              "type": "30",
              "iid": 4063280,
              "value": "XXXX"
            },
            {
              "type": "21",
              "iid": 4063265,
              "value": "2JCIE-BU01"
            },
            {
              "type": "23",
              "iid": 4063267,
              "value": "Rbt [XXXX]"
            },
            {
              "type": "52",
              "iid": 4063314
            },
            {
              "type": "53",
              "iid": 4063315
            }
          ]
        }
      ]
    }
  ]
}

さらに、次に示すトピックにcharacteristicsと書かれたJSONが送られて、これを確認することであるトピックでやりとりされる値の意味を把握できます。

  • ${環境センサのUUID}/services
  • ${スマートメーターのUUID}/services

次からはそれぞれに送られるデータの詳細について述べます。

環境センサ

${環境センサのUUID}/servicesには次に示すJSONが送られています。

また長いので折りたたみ(一部編集)
[
  {
    "type": "3e",
    "iid": 4063232,
    "characteristics": [
      {
        "type": "20",
        "iid": 4063264,
        "value": "OMRON",
        "perms": [
          "pr"
        ],
        "format": "string"
      },
      {
        "type": "30",
        "iid": 4063280,
        "value": "XXXX",
        "perms": [
          "pr"
        ],
        "format": "string"
      },
      {
        "type": "21",
        "iid": 4063265,
        "value": "2JCIE-BU01",
        "perms": [
          "pr"
        ],
        "format": "string"
      },
      {
        "type": "23",
        "iid": 4063267,
        "value": "Rbt [XXXX]",
        "perms": [
          "pr",
          "pw"
        ],
        "format": "string"
      },
      {
        "type": "52",
        "iid": 4063314,
        "perms": [
          "pr"
        ],
        "format": "string"
      },
      {
        "type": "53",
        "iid": 4063315,
        "perms": [
          "pr"
        ],
        "format": "string"
      }
    ]
  },
  {
    "type": "ef02",
    "iid": 4009885696,
    "characteristics": [
      {
        "type": "fe21",
        "iid": 4009950753,
        "perms": [
          "pr",
          "pw"
        ],
        "description": "2JCIE-BU01 error status",
        "format": "int",
        "minValue": 0,
        "maxValue": 65535,
        "minStep": 1
      },
      {
        "type": "11",
        "iid": 4009885713,
        "perms": [
          "pr",
          "ev"
        ],
        "description": "Temperature",
        "format": "float",
        "unit": "celsius",
        "minValue": -40.0,
        "maxValue": 125.0,
        "minStep": 0.01
      },
      {
        "type": "10",
        "iid": 4009885712,
        "perms": [
          "pr",
          "ev"
        ],
        "description": "Relative humidity",
        "format": "float",
        "unit": "percentage",
        "minValue": 0.0,
        "maxValue": 100.0,
        "minStep": 0.01
      },
      {
        "type": "fe10",
        "iid": 4009950736,
        "perms": [
          "pr",
          "ev"
        ],
        "description": "Ambient Light",
        "format": "int",
        "unit": "lx",
        "minValue": 0,
        "maxValue": 30000,
        "minStep": 1
      },
      {
        "type": "de13",
        "iid": 4009942547,
        "perms": [
          "pr",
          "ev"
        ],
        "description": "Barometric pressure",
        "format": "float",
        "unit": "hPa",
        "minValue": 300.0,
        "maxValue": 1100.0,
        "minStep": 0.001
      },
      {
        "type": "fe12",
        "iid": 4009950738,
        "perms": [
          "pr",
          "ev"
        ],
        "description": "Sound Noise",
        "format": "float",
        "unit": "dB",
        "minValue": 33.0,
        "maxValue": 120.0,
        "minStep": 0.01
      },
      {
        "type": "fe15",
        "iid": 4009950741,
        "perms": [
          "pr",
          "ev"
        ],
        "description": "eTVOC",
        "format": "int",
        "unit": "ppb",
        "minValue": 0,
        "maxValue": 32767,
        "minStep": 1
      },
      {
        "type": "fe16",
        "iid": 4009950742,
        "perms": [
          "pr",
          "ev"
        ],
        "description": "eCO2",
        "format": "int",
        "unit": "ppm",
        "minValue": 0,
        "maxValue": 32767,
        "minStep": 1
      },
      {
        "type": "fe13",
        "iid": 4009950739,
        "perms": [
          "pr",
          "ev"
        ],
        "description": "Discomfort index",
        "format": "float",
        "minValue": 0.0,
        "maxValue": 100.0,
        "minStep": 0.01
      },
      {
        "type": "fe14",
        "iid": 4009950740,
        "perms": [
          "pr",
          "ev"
        ],
        "description": "Heat stroke",
        "format": "float",
        "unit": "celsius",
        "minValue": -40.0,
        "maxValue": 125.0,
        "minStep": 0.01
      },
      {
        "type": "fe17",
        "iid": 4009950743,
        "perms": [
          "pr",
          "ev"
        ],
        "description": "Vibration information",
        "format": "int",
        "minValue": 0,
        "maxValue": 2,
        "minStep": 1
      },
      {
        "type": "fe18",
        "iid": 4009950744,
        "perms": [
          "pr",
          "ev"
        ],
        "description": "SI value",
        "format": "float",
        "unit": "kine",
        "minValue": 0.0,
        "maxValue": 6553.5,
        "minStep": 0.1
      },
      {
        "type": "fe20",
        "iid": 4009950752,
        "perms": [
          "pr"
        ],
        "description": "2JCIE-BU01 Latest sensing data",
        "format": "string"
      },
      {
        "type": "fe22",
        "iid": 4009950754,
        "perms": [
          "pr"
        ],
        "description": "2JCIE-BU01 Latest calculation data",
        "format": "string"
      }
    ]
  }
]

ここで注目したいのは次のようなところです。

      {
          :
        "iid": 4009885713,
        "description": "Temperature",
        "format": "float",
        "unit": "celsius",
        "minValue": -40.0,
        "maxValue": 125.0,
        "minStep": 0.01
      },

これは${環境センサのUUID}/${iid}のトピックに${description}のデータが送られることを示しています。 この例だと${環境センサのUUID}/4009885713に温度データが送られます。 formatunitなどの情報は厳格に守られているかというと微妙ですが(詳細後述)、だいたいは合っていそうです。

主要なiiddescriptionを抜き出し、説明も書いてみるとこんな感じです。

characteristics = {
  '4009885713': 'Temperature',           # 温度
  '4009885712': 'Relative humidity',     # 湿度
  '4009950736': 'Ambient Light',         # 照度
  '4009942547': 'Barometric pressure',   # 気圧 
  '4009950738': 'Sound Noise',           # 騒音
  '4009950741': 'eTVOC',                 # 総揮発性有機化合物(TVOC)濃度相当値
  '4009950742': 'eCO2',                  # CO2濃度(eTVOC値から算出)
  '4009950739': 'Discomfort index',      # 不快指数
  '4009950740': 'Heat stroke',           # 熱中症警戒度
  '4009950743': 'Vibration information', # 振動情報
  '4009950744': 'SI value',              # SI値(地震動の破壊エネルギーの大きさ)
}

製品情報の「出力データ」と同じっぽいですね。 ただ、Vibration informationSI valueについては環境センサ側の動作モードが違うためか、送られてきませんでした。 それ以外の情報は、次に示すように値とUnixtime(ミリ秒)を含んだJSONデータが1分おきに送られてきます。

4009885713 UNKNOWN b'{"msg_id":0,"value":19.81,"timestamp":1707896121359}\x00'
4009885713 UNKNOWN b'{"msg_id":0,"value":19.9,"timestamp":1707896181252}\x00'

スマートメーター

次はスマートメーターの話です。 こちらも環境センサと同じような感じで、${スマートメーターのUUID}/servicesに次のようなJSONが送られてきます。

これも長いので折りたたみ(一部編集)
[
  {
    "type": "3e",
    "iid": 4063232,
    "characteristics": [
      {
        "type": "20",
        "iid": 4063264,
        "value": "NextDrive Inc.",
        "perms": [
          "pr"
        ],
        "format": "string"
      },
      {
        "type": "30",
        "iid": 4063280,
        "value": "SM12345678",
        "perms": [
          "pr"
        ],
        "format": "string"
      },
      {
        "type": "21",
        "iid": 4063265,
        "value": "SmartMeter",
        "perms": [
          "pr"
        ],
        "format": "string"
      },
      {
        "type": "23",
        "iid": 4063267,
        "value": "SM-XXXX",
        "perms": [
          "pr",
          "pw"
        ],
        "format": "string"
      }
    ]
  },
  {
    "type": "288",
    "iid": 42467328,
    "characteristics": [
      {
        "type": "8880",
        "iid": 42502272,
        "perms": [
          "pr",
          "pw"
        ],
        "description": "public.nap.characteristic.wisun.operation-status",
        "format": "bool",
        "minValue": 0,
        "maxValue": 1,
        "minStep": 1
      },
      {
        "type": "88d3",
        "iid": 42502355,
        "perms": [
          "pr",
          "ev"
        ],
        "description": "public.nap.characteristic.smart-meter.coefficient",
        "format": "int",
        "minValue": 0,
        "maxValue": 999999,
        "minStep": 1
      },
      {
        "type": "88d7",
        "iid": 42502359,
        "perms": [
          "pr",
          "ev"
        ],
        "description": "public.nap.characteristic.smart-meter.number",
        "format": "int",
        "unit": "digit",
        "minValue": 1,
        "maxValue": 8,
        "minStep": 1
      },
      {
        "type": "88e1",
        "iid": 42502369,
        "perms": [
          "pr"
        ],
        "description": "public.nap.characteristic.smart-meter.unit",
        "format": "float",
        "unit": "kWh",
        "minValue": 0.0001,
        "maxValue": 10000,
        "minStep": 10
      },
      {
        "type": "88e0",
        "iid": 42502368,
        "perms": [
          "pr",
          "ev"
        ],
        "description": "public.nap.characteristic.smart-meter.cumulative-amount-electric-energy-n",
        "format": "float",
        "unit": "kWh",
        "minValue": 0,
        "maxValue": 99999999,
        "minStep": 0.0001
      },
      {
        "type": "88e3",
        "iid": 42502371,
        "perms": [
          "pr",
          "ev"
        ],
        "description": "public.nap.characteristic.smart-meter.cumulative-amount-electric-energy-r",
        "format": "float",
        "unit": "kWh",
        "minValue": 0,
        "maxValue": 99999999,
        "minStep": 0.0001
      },
      {
        "type": "88e7",
        "iid": 42502375,
        "perms": [
          "pr",
          "ev"
        ],
        "description": "public.nap.characteristic.smart-meter.instantaneous-electric-energy",
        "format": "int",
        "unit": "watt",
        "minValue": -2147483647,
        "maxValue": 2147483647,
        "minStep": 1
      },
      {
        "type": "88e8",
        "iid": 42502376,
        "perms": [
          "pr",
          "ev"
        ],
        "description": "public.nap.characteristic.smart-meter.instantaneous-electric-currents-r",
        "format": "float",
        "unit": "A",
        "minValue": -3276.7,
        "maxValue": 3276.5,
        "minStep": 0.1
      },
      {
        "type": "88f8",
        "iid": 42502392,
        "perms": [
          "pr",
          "ev"
        ],
        "description": "public.nap.characteristic.smart-meter.instantaneous-electric-currents-t",
        "format": "float",
        "unit": "A",
        "minValue": -3276.7,
        "maxValue": 3276.5,
        "minStep": 0.1
      },
      {
        "type": "88f0",
        "iid": 42502384,
        "perms": [
          "pw",
          "ev"
        ],
        "description": "public.nap.characteristic.smart-meter.b-route-id",
        "format": "string"
      },
      {
        "type": "88f1",
        "iid": 42502385,
        "perms": [
          "pw",
          "ev"
        ],
        "description": "public.nap.characteristic.smart-meter.b-route-password",
        "format": "string"
      },
      {
        "type": "88f3",
        "iid": 42502387,
        "perms": [
          "pr",
          "ev"
        ],
        "description": "public.nap.characteristic.smart-meter.rssi",
        "format": "int",
        "unit": "dBM",
        "minValue": -128,
        "maxValue": 127,
        "minStep": 1
      }
    ]
  }
]

こちらも${iid}のトピックにデータが送られてくるので、また主要なiiddescriptionを抜き出してみるとこんな感じです。

characteristics = {
  '42502272': 'public.nap.characteristic.wisun.operation-status',                          # 動作状態 (Boolean)
  '42502355': 'public.nap.characteristic.smart-meter.coefficient',                         # 積算値の換算係数
  '42502359': 'public.nap.characteristic.smart-meter.number',                              # 積算電力量有効桁数
  '42502369': 'public.nap.characteristic.smart-meter.unit',                                # 積算電力量単位
  '42502368': 'public.nap.characteristic.smart-meter.cumulative-amount-electric-energy-n', # 正方向(買電)積算電力量
  '42502371': 'public.nap.characteristic.smart-meter.cumulative-amount-electric-energy-r', # 逆方向(売電)積算電力量
  '42502375': 'public.nap.characteristic.smart-meter.instantaneous-electric-energy',       # 瞬時電力
  '42502376': 'public.nap.characteristic.smart-meter.instantaneous-electric-currents-r',   # R相瞬時電流
  '42502392': 'public.nap.characteristic.smart-meter.instantaneous-electric-currents-t',   # T相瞬時電流

  '42502387': 'public.nap.characteristic.smart-meter.rssi', # 後述
}

上のほうはECHONET Liteの「低圧スマート電力量メータクラス規定」にならったものですね。 (この辺はNextDrive社の競合?であるNature社の開発者向けドキュメントでもわかりやすく説明されていました。) Ecogineアプリからは見えなかった瞬時電力も取れるのが良いです6。 これらのデータは1分おき(積算電力量は30分おき)に送られてきます。

ただ、このデータは元々の「低圧スマート電力量メータクラス規定」とは若干違う箇所もあるようです。 具体的には数値形式が異なること(規定ではIntegerとなっているが、Cube J1からはFloatで来るなど)と、coefficientnumberおよびunitが送られて来ないという点です。 coefficientについては任意(デフォルト1とみなす)のため別に良いとしても、numberunitが無いと本来であれば正しい積算電力量の計算が出来ません。 しかし、実際に電力量計器と照らし合わせるとCube J1から送られてくる積算電力量は合っているように見えます。 もしかすると、スマートメーターから送られてくる生の値をCube J1くんが下処理してからMQTTに流してくれているのかも知れません。

一番下のrssiはCube J1が独自に付けてくるデータのようです。 これは名前のとおりRSSIでしょう。 EcogineアプリでもスマートメーターとWiSUNで接続する際の強度が表示されていました。 長いservicesのJSONを見ていくと-128~127の範囲で表され、単位はdBMということですねと。

      {
          :
        "iid": 42502387,
        "description": "public.nap.characteristic.smart-meter.rssi",
        "format": "int",
        "unit": "dBM",
        "minValue": -128,
        "maxValue": 127,
        "minStep": 1
      }

単位はdBmの誤記と思われますがまぁ良いでしょう。 一応実際のデータも見てみます。

42502387 UNKNOWN b'{"msg_id":0,"value":200,"timestamp":1707900336394}\x00'
42502387 UNKNOWN b'{"msg_id":0,"value":199,"timestamp":1707900396415}\x00'
42502387 UNKNOWN b'{"msg_id":0,"value":199,"timestamp":1707900406763}\x00'

いやなんか200とか199とか書いているんですけど……ちなみにこのときのEcogineアプリでは-49[dBm]という表記になっていました。 まぁこういうのはuint8とint8を間違えただけでしょう。 慌てず騒がず落ち着いて、優雅な感じで上品に、200を2進数に変換して2の補数を取ると……いやどうしても-49にはならない! たまに199になるので200固定のダミーデータというわけでもない! もし本当に200[dBm]だったらこれに関する電力だけで5000兆[W]の4000万倍かかっていることになってしまう。

これはおかしいということで、Ecogineアプリが行っている処理を再度確認すると

  • このrssiはRSSIではなく、LQI (Link Quality Indicator) の値を示す
  • このデバイスにおいて、LQIから電界強度[dBm]への変換は (LQI * 0.275) - 104.2 で行う

ということが分かりました。 LQI(値が大きいほうが良い)ということで別にこの値のまま評価しても良いですね。 一応電界強度に変換しておくと、LQIが200のときは-49.2[dBm]となり、Ecogineアプリ側の表示と揃いました。

おわりに

NextDrive Cube J1からどうにかこうにかセンサデータを取得しました。 当初はEcogineアプリで取れる範囲のデータが取れたら良いなと思っていましたが、最終的にはMQTTからそれ以上のデータを取ることができ満足です。

ただ、冒頭でも述べたとおりCube J1とEcogineアプリはサポートが終了しており、いつまで使えるかは分かりません。 もしEcogineアプリが使えなくなった場合に備え、Cube J1の初期セットアップに関して叩かれる/nextdrive.apiのリクエストを記録しておいた方が良いかな……という気は若干します。 (だがまだその時ではない)

将来的にCube J1自体も使えなくなったら、WiSUNモジュール (ROHM BP35C0) だけ取り出して頑張ります。

Footnotes

  1. Cube JCube J1と見た目の似たデバイス…というか、最初にCube JがありネコリコホームプラスのためにカスタムされたのがCube J1という感じがします。

  2. https://twitter.com/346takahiro/status/1512784295982641156

  3. https://twitter.com/346takahiro/status/1512026241989513222

  4. アプリ側がURLを知る必要があるのかは不明です。/nextdrive.api経由でデバイスにファームウェアのバイナリ自体を渡せそうな機能もあるように見えてそのため?かも知れません。なお、過去のNextDrive製品ではユーザがUSBメモリに保存しデバイスに読み込ませることで更新も行えたようです。Cube J1でも出来るかは未確認ですが、もし可能でさらに改変したファームウェアを読み込ませることも可能かも……知れません。

  5. EMBAも使ってみましたが、結果的にはbinwalkのみで十分でした。

  6. R相、T相の電流も取れるのが良いです(ただし1A単位であり0.1Aは測れない模様)。バランスの確認が捗ります。これまではわざわざクランプメーターで見ていて手間でした。