이 글은 내가 작업한 내용을 토대로 작성하였고, 그대로 진행할 때에 오류가 발생할 수 있으며 굳이 똑같이 진행하지 않아도 다른 방법들이 있을 수 있다.

참고한 문서는 zigbee2mqtt의 how_to_sniff_zigbee_traffic 이다.

준비물 : CC2531, CC Debugger(CC2531 플래시용 - 라즈베리 등으로 대체 가능), GUI 리눅스 머신, Wireshark 소프트웨어

맥을 사용하고 있지만 맥에서는 제대로 안되는 것 같아서 리눅스를 가상머신에서 띄우고 거기서 작업하였다. (Synolgy NAS에서 스니퍼를 인식한다면 가능할 것도 같지만 시놀로지는 여러 리눅스 패키지들이 삭제되어 있어 씨름하기 싫어서 그냥 가상머신 사용했다.)

플래시는 맥에서 진행하였다.
TI사이트에서 PACKET-SNIFFER를 다운받는다. 2가 아닌 1버전이어야 한다. exe 파일이 다운로드 되지만, exe안에 포함된 펌웨어 파일이 필요하기 때문에 다운받은 파일을 7zip 같은 exe 파일 분리 프로그램으로 펌웨어를 뽑아낸다. 경로는 bin/general/firmware/sniffer_fw_cc2531.hex 이다.
이것을 CC Debugger를 통해 CC2531에 플래싱한다.(플래싱도 역시 여기 참조) 이렇게 되면 스틱은 준비 완료.

스틱을 통한 패킷을 캡쳐하기 위해 리눅스머신을 준비하고, whsniff라는 프로그램을 설치해야한다. (gui가 아닌 cli에서 캡쳐를 진행해보지 않았다.)
리눅스의 터미널을 열어 아래와 같이 한줄한줄 실행한다.

cd /opt
sudo apt-get install -y libusb-1.0-0-dev wireshark
curl -L https://github.com/homewsn/whsniff/archive/v1.1.tar.gz | tar zx
cd whsniff-1.1
make
sudo make install

위 명령어의 두 번째 라인에서 wireshark도 같이 설치되었다.

이제 캡쳐하려는 zigbee 네트워크를 준비(?)한다. 캡쳐에 필요한 정보는 zigbee 채널 번호와 두 개의 네트워크의 암호화 키이다.

일단 zigbee 연결에 기본적으로 사용되는 암호화 키인 Trust Center link key를 등록해준다.

Wireshark를 열고, Edit -> Preferences -> Protocols -> ZigBee를 선택하고 Pre-configured Keys의 Edit를 해준다.

wireshark-preferences
wireshark-preferences-zigbee

<이미지를 클릭하면 크게 볼 수 있습니다>

5A:69:67:42:65:65:41:6C:6C:69:61:6E:63:65:30:39, Byte Order: Normal 로 새로운 값을 추가한다. (Hue Bridge는 Trust Center Link Key가 다르다고 한다. 위 how_to_sniff_zigbee_traffic 문서에 링크가 있다.)

wireshark-preferences-keys

그리고 추가로 암호화 키를 하나 더 입력하여야 하는데, 이것은 각 허브마다 지정되어 있는 값이 있기 때문에 직접 찾아내야 한다.
zigbee2mqtt의 경우 암호화 키를 설정 가능하기 때문에 설정파일에 있는 키를 찾아 입력하면 되지만, 기성 허브의 경우엔 다른 방법으로 찾아내야 한다. 그 값을 찾아내기 위해 zigbee 디바이스 하나를 준비하고 패킷 캡쳐를 시작한다.

일단 사용하고 있는 허브의 Zigbee 채널을 확인하고, 패킷 캡쳐를 시작한다. 아래 명령어의 25 대신에 자신이 사용하는 Zigbee 채널을 입력한다.

sudo whsniff -c 25 | wireshark -k -i -

wireshark 창이 뜨면서 패킷 캡쳐가 시작된다. 시작되면 허브의 페어링모드를 시작하고 디바이스 (리)페어링을 한다. 페어링이 완료되면 일단 캡쳐는 중단한다. 어차피 확인하기 어려운 패킷이기 때문에 의미없다.

프로토콜이 ZDP 인 것을 필터링한다. 패킷 내용의 Zigbee Security Header의 값이 필요하다.

이미지의 파랗게 선택된 라인의 [Key: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6] 이런 값을 복사하여 아까의 암호화 키 형태로 두 문자 사이에 :를 추가하여 A1:B2:C3:D4:E5:F6:G7:H8:I9:J0:K1:L2:M3:N4:O5:P6 처럼 수정하여 Pre-configured Keys에 추가한다.

이렇게 하면 허브의 Zigbee 패킷은 모두 확인할 수 있게 된다.
키 두 개 모두 추가하지 않은 상황에서는 통신하는 내용이 암호화되어 확인할 수 없으니 두 키를 입력 후에 다시 패킷 캡쳐를 시작하고 디바이스를 컨트롤하면 된다.

패킷 캡쳐 설명에 사용된 기기는 샤오미 전동 커튼 B1 모터이다. 패킷 캡쳐로 실제 DTH도 만들었고, 샤오미 기기만의 다른 부분이 있어서 이걸로 설명하겠다. 샤오미 게이트웨이에 연결 후 위치를 변경하고, 각종 커튼의 설정값을 변경해보며 캡쳐하였다.

Zigbee 커맨드는 Hubitat에 맞춰 작성한 것이며, Smartthings도 유사하나 정확한 것은 Smartthings의 문서에서 추가 확인이 필요하다. 여기서 DTH의 상세 테크닉은 여기서 다루지 않을 예정이다. 패킷 캡쳐를 통해 획득한 정보를 DTH 커맨드로 어떻게 변환하는지 정도만 설명한다.

Zigbee HA 패킷을 필터링을 하기 위해 필터에 zbee_nwk 로 필터링한다.

필터를 적용하면 Zigbee와 Zigbee HA프로토콜의 패킷만 보인다.
그 중에 Info 컬럼을 보면 ZCL:Write Attributes, ZCL:Report Attributes, ZCL:Read Attributes, ZCL:Read Attributes Response 가 보인다.

이 4종류의 패킷을 이용해서 DTH를 만들 수 있다.

Info 내용 설명
Read Attributes 기기의 정보를 읽기 위하여 보내는 신호
Read Attributes Response 기기의 정보를 읽기 신호에 대한 응답 - 기기의 상태가 포함되어 있음
Write Attributes 기기의 데이터를 쓰기 위하여 보내는 신호
Report Attributes 기기 상태 변화값을 리포트 한 신호

먼저 Read Attributes를 살펴보면

필요한 부분은 Zigbee Application Support Layer Data의 Cluster와 Zigbee Cluster Library Frame의 Attribute Field 정보이다.

Zigbee Cluster Library (ZCL)을 알아두면 좋다. 0x0000 옆에 Basic으로 표기가 되어있는데, ZCL에 미리 정의된 값들이 있다. 전원, 센서 메트릭, 전력 측정 등 정해진 값이 있다. 보통의 표준을 지킨 디바이스들은 라이브러리에서 지정한 클러스터 번호와 Attribute를 사용하여 통신하기 때문에 굳이 패킷을 캡쳐하지 않아도 DTH를 만들 수 있다.

이미지에서 Cluster 0x0000, Attribute 0x0001 이다. DTH의 Zigbee 신호 커맨드에서는 이렇게 된다.

zigbee.readAttribute(0x0000, 0x0001)

Attribute의 0x0001이 Application Version에 대한 속성이기 때문에, 이 속성에 대해 읽기를 요청하면 모터에 설치된 버전 정보를 알 수 있다.

이번엔 Write Attributes에 패킷을 보면,

역시 동일하게 Cluster와 Attribute Field 정보를 확인한다.

Cluster : 0x000d
Attribute : 0x0055
Data Type : 0x39
Data값은 Float 24이다.
Write의 경우 코드로 변경하면 아래처럼 된다.

zigbee.writeAttribute(0x000d, 0x0055, 0x39, 24)

0x000d 클러스터의 0x0055 속성은 아날로그 값을 변경하기 위한 커맨드이고, 커튼에서 명령어의 밸류를 커튼의 위치로 사용하고 있다. 커튼 위치의 아날로그의 범위는 0-100까지 가능하다. 위 커맨드는 커튼의 위치를 24%까지 움직이라는 명령어가 된다.

Smartthings의 Device Type Handler에서는 4번째 파라메터의 밸류를 16진수(Hex string)으로 변경해서 사용하고 있기도하니 각 허브에서 어떤식으로 사용하는지 확인하여야 한다.

Read Attributes를 통해서 기기에서 응답하는 데이터인 Read Attributes Response를 본다.

앞서 본 Read Attributes 패킷의 Read 0x0000, 0x0001의 결과이다.

Cluster가 0x0000이고 Attribute가 0x0001이다. 데이터의 타입이 8-Bit Unsigned Int(0x02)이고, 데이터는 12(0c) 이다.

마지막으로, 남은 Report Attributes를 보자. 이 패킷은 디바이스에 따라 발생하지 않을 수 있다. 디바이스가 센싱 정보가 리포팅 되지 않는 스펙이라면 패킷이 발생되지 않는다.

Cluster가 0x000d이고, 첫 번째 Attribute Field의 Attribute가 0x0055이다. 데이터의 타입이 Single Precision Float(0x39)이고, 데이터는 24 이다.
앞서 Write Attributes의 24%까지 위치를 지정한 데이터 쓰기 이후 커튼이 동작이 완료된 뒤 자신의 상태를 report 한 것이다.

위 Report Attributes 패킷의 경우에 Attribute Field가 두 개이다. 첫 번째 Attribute가 메인 값이고, 두 번째가 additional attribute로 확인이 가능하다.

Read Attributes Response나 Report Attributes는 디바이스로부터 허브로 데이터가 전송되는 것이기 때문에, DTH의 parse 메소드로 데이터가 전송되어 확인이 가능하다.

위의 패킷에 대응하는 코드를 보자.

def parse(String description) {
	log.trace "Description is $description"

	def event = zigbee.getEvent(description)
	def msg = zigbee.parseDescriptionAsMap(description)

    // 허비탯은 event가 지원이 되는지 확인 불가능. 사용했던 디바이스들은 모두 event가 없었다.
	log.trace "Parsed event: ${event}, msg: ${msg}"

	if (msg?.cluster == "0000") {
		if (msg?.attrId == "0001") {
            // msg.value는 0c로 되어있을 것이다. 필요에 따라 10진수로 변경하여 사용하면 된다.
			log.debug "Device application version : ${msg.value}"
		}
    } else if (msg?.cluster == "000D") {
        if (msg?.attrId == "0055") {
            long longValue = Long.parseLong(msg.value, 16)
            float position = Float.intBitsToFloat(longValue.intValue())
			log.debug "Device position : ${position}"
		}
	}
} 

허브로 데이터가 들어오는 때 모두 parse 메소드가 호출되기 때문에 각 메시지에 따라, 데이터를 분석하여 허브의 디바이스 상태를 변경할 수 있도록 하여야 한다.

Write Attributes Response 패킷도 있으나 보통 Success로 응답이 올 것이고, 허브에서는 무시되는 것 같다.

추가로 다른 Write Attributes를 확인해보자.

첫 번째 패킷의 내용은 모터의 이동 방향을 변경하는 패킷이다. 두 번째 패킷은 앞서 봤던 위치 변경 패킷이다.
첫 번째 패킷을 커맨드로 작성하면 아래와 같다.

zigbee.writeAttribute(0x0000, 0xff28, 0x01, 0)
zigbee.writeAttribute(0x0000, 0xff28, 0x01, 1)

이렇게 하여 DTH에서 동작시키면 모터의 방향이 변경되지 않는다. 자세히 보면 두 개의 패킷 사이에 데이터 말고 조금 다른 부분이 있다.
Frame Control FieldManufaturer Specific: True/False 가 다르다. Frame Control Field 역시 Zigbee 패킷에 대해 알아두면 좋다.

Manufacturer Specific 값을 전송하기 위해 아래처럼 하여야 한다.

zigbee.writeAttribute(0x0000, 0xff28, 0x01, 0, [mfgCode: "0x115f"])
zigbee.writeAttribute(0x0000, 0xff28, 0x01, 1, [mfgCode: "0x115f"])

5번째 파라메터에 mfgCode값을 별도로 전달해주어야 허브에서도 Manufacturer Specific 하도록 패킷을 보낼 수 있는 것이다. mfgCode값은 패킷의 Manufacturer Code: Unknown (0x115f)를 참고하여 입력하면 된다.

그리고 3 번째 파라메터의 인코딩 값은 스마트싱스 문서에 잘 정리가 되어있어 링크를 걸겠다. Smartthings Document

그리고 아직 B1 모터의 미해결점이 있는데, ST나 Hubitat에 연결하면 커튼 동작 후 위치 리포팅이 제대로 안되고 있다.
샤오미 게이트웨이에서는 앞서 본 캡쳐 내용처럼 정확히 24%로 리포팅이 되고 있으나 다른 허브에 연결된 경우에는 2%로 리포팅이 된다. 이 부분을 해결할 수 있는 고수가 나타나길 기대하고 있다.


내년엔 제대로 Zigbee 3.0 센서들이 많이 출시될 것 같은데 위 방법대로는 3.0이 캡쳐되는지 잘 모르겠다.


가끔 패킷 캡쳐를 하다보면 Permission Denied 에러나 다른 에러메시지가 발생하곤 하는데, 이런 경우

chmod 755 /usr/bin/dumpcap

를 실행해주거나, wireshark도 sudo로 실행하거나, 리눅스 머신을 재시작해보거나 한다. 정확한 해결법은 찾아보지 않았다.

나중에라도 오류가 확인된다면, 수정될 수 있다.