野次馬エンジニア道

野次馬な気持ちでプログラミングをあれこれと綴ります

IKEA TRÅDFRIをZigbeeでGoogle Home から操作

のエントリでIKEA TRÅDFRIをThings Gatewayから操作できるようになった。

せっかくなので

  • OK Google ダイニングのライトをつけて
  • OK Google ダイニングのライトを消して
  • OK Google ダイニングを60%の明るさにして

こんな感じでGoogle Home経由で操作できるようにする。

IKEA TRÅDFRIのファクトリーリセットからメッシュのネットワークの構築まではThings Gatewayに任せて、 Google HomeのIFTTTが設定してある既存Raspberry PiXbeeのドングルを差し替える。

f:id:notta55:20180506080136j:plain:w400

流れとしては、

という感じ。

前準備 : 操作可能な機能の調査

Things Gatewayのログ*1を見ながら、デバイスの情報を調べてみる。

ZDOのSimple Descriptor Resp

ZDOとはネットワークを管理している共通のZigBee Device Profile ( ProfileID:0x0000 ) のエンドポイントのこと。 詳しくはこちら

Rcvd: Explicit Rx 90fdxxxxxxxxxxxxxxxxxx ZDO 8004 Simple Descriptor Resp (0x8004) status: success (0)
Rcvd: { 
    type: 145,
    remote64: '90fdxxxxxxxxxxxxxxxxxx',
    remote16: '2637',
    sourceEndpoint: '00',
    destinationEndpoint: '00',
    clusterId: '8004',
    profileId: '0000',
    receiveOptions: 1,
    data: <Buffer 05 00 37 26 22 01 5e c0 20 02 02 09 00 00 03 00 04 00 05 00 06 00 08 00 00 03 05 0b 00 10 04 05 00 19 00 20 00 00 10>,
    zdoSeq: 5,
    status: 0,
    zdoAddr16: '2637',
    simpleDescriptorLength: 34,
    endpoint: 1,
    appProfileId: 'c05e',
    appDeviceId: '0220',
    appDeviceVersion: 2,
    inputClusterCount: 9,
    inputClusters: [ '0000','0003','0004','0005','0006','0008','0300','0b05', '1000' ],
    outputClusterCount: 4,
    outputClusters: [ '0005', '0019', '0020', '1000' ] }

ここからIKEA TRÅDFRI がどんなデバイスなのかが分かる。

Field ID
profileId c05e ZigBee Light Link Profile
appDeviceId 0220 Lighting devicesのColor temperature light
Input Cluster Basic(0000), Identity(0003), Groups(0004), Scene(0005), On/off(0006), LevelControl(0008), Color control(0300), Diagnostics(0b05), ZLL commissioning(1000)
output Cluster Scene(0005), OTA Upgrade ? (0019), PollControll(0020), ZLL commissioning(1000)

ZCL - ColorCapabilities

続いてZigbee Cluster Libraryの値を見てみる。

Rcvd: Explicit Rx 90fdxxxxxxxxxxxxxxxxxx ZHA 0300 lightingColorCtrl readRsp [ { attrId: 400A, status: 0, dataType: 25, attrData: 16 } ]
Rcvd: { 
    type: 145,
    remote64: '90fdxxxxxxxxxxxxxxxxxx',
    remote16: '2637',
    sourceEndpoint: '01',
    destinationEndpoint: '00',
    clusterId: '0300',
    profileId: '0104',
    receiveOptions: 1,
    data: <Buffer 18 06 01 0a 40 00 19 10 00>,
    zcl: 
    { frameCntl: { frameType: 0, manufSpec: 0, direction: 1, disDefaultRsp: 1 },
      manufCode: 0,
      seqNum: 6,
      cmdId: 'readRsp',
    payload: [ { attrId: 400A, status: 0, dataType: 25, attrData: 16 } ] } }
Field ID
clusterId 0300 Color Control
attrId 400A ColorCapabilities

dataTypeはbitmap(=16)。これはビット番号でサポートしている機能を表すようで、

  • 0: Hue/saturation supported, 1: Enhanced hue supported, 2: Color loop supported, 3: XY attributes supported, 4: Color temperature supported
  • 16=10000なので、Color temperatureのみのサポートとなっている*2

On/Offとレベルコントロールの実装

今回は、On/Off ( 0006 ) とレベルコントロール ( 0008 ) をサクッと実装。ZCLのIDやバイナリ部分の構築は

github.com

github.com

を使う。最低限のコードは下記となる。

const zcl = require('zcl-packet');
const zclId = require('zcl-id');
const xbee_api = require('xbee-api');
const C = xbee_api.constants;
const xbeeAPI = new xbee_api.XBeeAPI({api_mode: 1});

const ZHA_PROFILE_ID = toHex(zclId.profile('HA').value); //0104
const CLUSTER_ID_GENONOFF = toHex(zclId.cluster('genOnOff').value); //0006
const CLUSTER_ID_GENLEVELCTRL = toHex(zclId.cluster('genLevelCtrl').value); //0008
function toHex(v) { return ('0000' + v.toString(16)).substr(-4); }

function buildZclFrame(addr64, addr16, endpoint, profileId, clusterId, zclData) {

    checkAndFillProps(zclData);

    let frame = {
        id: xbee_api._frame_builder.nextFrameId(),
        type: C.FRAME_TYPE.EXPLICIT_ADDRESSING_ZIGBEE_COMMAND_FRAME,
        destination64: addr64,
        destination16: addr16,
        sourceEndpoint: 0,
        destinationEndpoint: endpoint,
        profileId: profileId,
        clusterId: clusterId,
        broadcastRadius: 0,
        options: 0,
        zcl: zclData,
    };
    frame.data = zcl.frame(zclData.frameCntl,
        zclData.manufCode,
        frame.id,
        zclData.cmd,
        zclData.payload,
        clusterId);
    return frame;
}

function checkAndFillProps(zclData){
    if (!zclData.frameCntl) {
        zclData.frameCntl = {frameType: 0};
    }
    if (zclData.frameCntl.manufSpec === undefined) {
        zclData.frameCntl.manufSpec = 0;
    }
    if (zclData.frameCntl.direction === undefined) {
        zclData.frameCntl.direction = 0;
    }
    if (zclData.frameCntl.disDefaultRsp === undefined) {
        zclData.frameCntl.disDefaultRsp = 0;
    }
    if (zclData.manufCode === undefined) {
        zclData.manufCode = 0;
    }
    if (zclData.payload === undefined) {
        zclData.payload = [];
    }
}

function makeOnOffValueProps(propertyValue) {
    let attr = propertyValue ? 'on' : 'off';
    return {
        frameCntl: {frameType: 1},
        cmd: attr,
    };
}

function makeLevelValueProps(propertyValue) {
    let level;
    if (propertyValue < 0.1) {
        propertyValue = 0;
    }
    level = Math.min(Math.round(propertyValue * 254 / 100), 254);
    return {
        frameCntl: {frameType: 1},
        cmd: 'moveToLevel',
        payload: [level]
    };
}

あとはIFFFTの設定(前回の記事と同じ)をすればOK。

動作確認 - Google Home から操作

  • OK Google ダイニングのライトをつけて
  • OK Google ダイニングのライトを消して
  • OK Google ダイニングを60%の明るさにして

上記の発話で下記のようにAPIのフレームを送信するようにサーバ側の実装を追加する。

xbeeAPI.builder.write(buildZclFrame(
    bulb.addr64,
    bulb.addr16,
    bulb.endpoint,
    ZHA_PROFILE_ID,
    CLUSTER_ID_GENONOFF,
    makeOnOffValueProps(true)
));

xbeeAPI.builder.write(buildZclFrame(
    bulb.addr64,
    bulb.addr16,
    bulb.endpoint,
    ZHA_PROFILE_ID,
    CLUSTER_ID_GENONOFF,
    makeOnOffValueProps(false)
));

xbeeAPI.builder.write(buildZclFrame(
    bulb.addr64,
    bulb.addr16,
    bulb.endpoint,
    ZHA_PROFILE_ID,
    CLUSTER_ID_GENLEVELCTRL,
    makeLevelValueProps(60)));
});

一通りZigbeeでの実現方法*3を理解したが、やはり機能を足すのは大変。 www.ikea.com が早く日本でもリリースされますように。。。

*1:https://github.com/mozilla-iot/wiki/wiki/Debugging-Zigbee

*2:IKEA TRÅDFRIのライトでThings Gatewayで色の変更ができないのがこれが原因。Hue/Saturationがいる

*3:正確にはLight LinkではなくHome Automationのプロファイルでですが