낡은 쪽은 기기가 아니라 그 값을 받아 적는 소프트웨어였다.
8월 18일 오전 11시, 야외에 둔 온습도계가 37.1도를 찍었다. 그날 실제 공기 최고는 오후 2시의 29도였다. 아침 해가 그대로 드는 자리라 플라스틱 통이 데워진 것이지, 공기가 37도였던 적은 없었다. 그 방수 지그비 온습도계 한 대가 우리 집 바깥 기준이었고, 그 값을 실내와 비교해서 창을 열지 말지 정하고 있었다.
그날 밤 센서를 볕이 들지 않는 곳으로 옮겼다. 햇빛은 막았지만 이번에는 저녁 값이 그 그늘진 공간의 공기가 됐다. 볕에 두면 공기가 아닌 것을 재고, 그늘로 넣으면 바깥이 아닌 것을 잰다. 이 문제의 오래된 해법이 백엽상이다. 학교 운동장 구석에 서 있던 그 하얀 상자, 햇빛은 막고 공기는 통하게 해서 온도계에 공기의 온도만 닿게 하는 물건이다. 요즘 기상 관측 센서에 달린 흰 통이 그것을 손바닥만 하게 줄여 놓은 것이다. 게다가 창을 열지 말지 정할 때는 바깥이 몇 도인지보다 지금 비가 오는지가 더 중요할 때도 많은데, 그건 온습도계로 알 길이 없다.
그래서 기상 관측 센서를 하나 들였다. Ecowitt WS90 Powered by Shelly, 온습도와 기압과 조도에 더해 UV와 바람과 비까지 일곱 가지를 한 대가 재는 물건이다. 연결은 싱겁게 끝났다. 허브에서 기기 추가를 열어 두고 CAL 버튼을 짧게 두 번 누르니 파란 LED가 한 번 깜빡였고, 곧 값이 들어오기 시작했다. 온도 32.3도, 습도 69퍼센트, 조도 24384럭스, 기압, 배터리.
값은 거기까지였고, 바람과 UV와 강우는 하나도 들어오지 않았다. 흰 통 덕분에 기온은 믿을 만해졌는데, 정작 바람과 비를 보려고 산 센서에서 그 두 값이 빠져 있었다. 그런데 연결이 워낙 쉬웠던 탓에 의심이 엉뚱한 데로 갔다. 통신이 멀쩡한데 값이 없다면 기기가 안 보내는 것 아니겠나. 오는 값을 표로 정리하고 그 아래에 안 오는 것이라는 칸을 만들어 풍속, 풍향, 강수, UV라고 적었다. 본체에 센서가 있어도 셀리 지그비 펌웨어는 안 보낸다는 이유까지 붙였다.
그 판단이 틀렸다는 것은 한참 뒤에 알았다. 칸에 적어 둔 네 개는 그때도 오고 있었다.
한 줄. 기기가 아니라 그 값을 받아 적는 소프트웨어가 낡아 있었다. Zigbee2MQTT 2.6.3에는 이 기기의 제조사 전용 클러스터를 읽는 해석기가 없었고, 그 해석기는 2.7.0에 들어왔다.
고친 방법. 컨테이너 태그를 2.13.0으로 올리고, 이미 페어링된 기기라 Reconfigure를 한 번 돌렸다. z2m이 내보내는 값이 6개에서 22개가 됐다. 재페어링도, 기기 펌웨어 작업도, 다른 플랫폼도 필요 없었다.
이야기를 이어 가려면 이 값들이 어디를 거쳐 오는지 한 번 봐야 한다. 지그비는 와이파이처럼 공유기 하나에 전부 매달리는 방식이 아니라, 기기들이 서로를 거쳐 신호를 전달하는 메시로 돈다. 여기서 역할이 셋으로 갈린다. 망을 여는 코디네이터가 하나 있고, 상시 전원으로 도는 기기는 라우터가 되어 남의 신호를 중계하고, 배터리로 도는 기기는 엔드 디바이스로 남아 자기 것만 보낸다. 배터리 기기가 중계까지 하면 배터리가 남아나지 않기 때문이다.
그래서 야외의 WS90은 코디네이터와 직접 말하지 않는다. 실내 스마트 플러그 하나가 라우터로 중간에 서 있고, 코디네이터는 이더넷으로 집 안 네트워크에 연결된다. 거기서부터는 소프트웨어 몫이다. Zigbee2MQTT가 지그비를 MQTT 메시지로 옮기고, 그 메시지를 익스포터가 메트릭으로 바꾸면 시계열 데이터베이스가 1분마다 긁어 가고, 그래프는 그 위에서 그려진다.
이 그림에서 미리 짚어 둘 것이 하나 있다. 값이 모자란 원인을 찾을 때 사람은 대개 왼쪽 끝, 그러니까 센서와 전파를 먼저 의심한다. 그런데 아래 첫 칸이 지그비의 말을 MQTT로 옮기는 번역 단계이고, 여기가 틀리면 무선이 아무리 멀쩡해도 값은 오지 않는다.
참고로 무선 쪽이 멀쩡했다는 근거는 나중에 따로 확인했다. 페이로드에 실려 오는 신호 품질 값이 255 만점에 15로 낮아 센서가 멀어서인가 싶었는데, 망 지도를 확인해 보니 WS90에서 라우터까지는 117로 넉넉했고 낮은 값은 라우터에서 코디네이터로 들어오는 마지막 구간에서 나왔다. 손을 대야 할 곳이 있다면 센서 쪽이 아니라 그 반대편이라는 뜻이고, 어느 쪽이든 이 글의 원인과는 무관했다.
Zigbee2MQTT는 zigbee2mqtt/bridge/devices 토픽에 기기 정보를 그대로 실어 보낸다. 그 기기가 무슨 기능을 갖췄는지도 함께 들어 있다. 꺼내 보다가 세 줄에서 눈이 멈췄다.
msTemperatureMeasurement, msPressureMeasurement, msRelativeHumidity,
manuSpecificLegrandDevices, (르그랑?)
manuSpecificSamsungAccelerometer, (삼성?)
manuSpecificPhilips2 (필립스?)
기상 센서 안에 프랑스 전기설비 회사와 한국 가전 회사와 네덜란드 조명 회사가 동시에 들어 있을 리 없다. 이게 왜 오역인지 알려면 지그비를 조금 알아야 하는데, 두 문단이면 된다.
지그비에서는 값의 의미가 클러스터라는 단위로 정해져 있다. 온도는 온도 클러스터, 습도는 습도 클러스터다. 표준에 없는 값을 보내고 싶은 회사는 제조사 전용으로 열린 번호대를 쓴다. 문제는 그 번호대를 회사끼리 나눠 갖지 않는다는 데 있다. 같은 번호를 르그랑도 쓰고 삼성도 쓰고 셀리도 쓴다. 누구 것인지는 무선 프레임에 같이 실려 오는 제조사 코드로만 갈린다.
그러니까 저 세 줄은, 셀리의 번호를 만난 번역기가 그게 셀리인 줄 모른 채 철자가 같은 다른 회사의 단어를 사전에서 집어 온 결과다. 진짜 정체는 0xFC01, 0xFC02, 0xFC03이고, 각각에 무엇이 담겼는지는 Shelly 기술문서에 적혀 있다.
| 클러스터 | 담는 값 |
|---|---|
0xFC01 | 풍속, 풍향, 돌풍 |
0xFC02 | UV 지수 |
0xFC03 | 강우 상태, 누적 강수량 |
같은 문서에 제조사 코드가 0x1490으로 적혀 있으니, 없던 것은 바람이 아니라 그것을 알아볼 사전이었던 셈이다.
Zigbee2MQTT를 설치한 날은 2026년 8월 14일이다. 그날 깐 것이 2.6.3인데, 2.6.3이 나온 날은 2025년 11월 1일이다. 같은 날 최신판은 2주 전에 나온 2.13.0이었다. 아홉 달 묵고 마이너 버전으로 일곱 개 뒤처진 것을 새로 깐 셈이다. 왜 그 태그였는지는 기록이 없다. 설치 노트에도 버전 숫자만 적혀 있고 고른 이유는 남아 있지 않다. 도커 태그를 손으로 고정해 두면 그 뒤로는 아무도 그 숫자를 다시 들여다보지 않게 되는데, 이번이 그런 경우였다.
WS90이 지원 목록에 들어온 것은 2025년 10월이다. 그때 정의는 표준 클러스터만 읽었다. 온도, 습도, 기압, 조도, 배터리로, 처음에 받은 목록과 정확히 같다. 바람과 UV와 비를 읽는 코드는 그다음 달에 들어왔다.
| 버전 | 날짜 | 무엇이 들어왔나 |
|---|---|---|
| 2.7.0 | 2025-12-01 | PR #10752. 바람과 UV와 강우 노출 |
| 2.9.0 | 2026-03-01 | PR #11437. 이슬점, 체감온도, 윈드칠, 열 스트레스, 강우 강도 같은 계산값 |
2.6.3에서 2.7.0으로는 한 판 차이다. 바람과 UV와 강우를 읽는 코드는 설치 시점에 이미 여덟 달 반 동안 나와 있었는데, 하필 그 직전 판을 골라 깔아서 한 판 차이로 놓쳤다. 기기 지원을 된다와 안 된다로만 보면 이런 경우를 놓친다. 지원 목록에 이름이 올라와 있어도 어디까지 읽느냐는 버전마다 다르고, 그 경계는 릴리스 노트를 열기 전에는 보이지 않는다.
정작 고치는 일은 compose 파일에서 이미지 태그 한 줄을 바꾸는 것으로 끝났다.
zigbee2mqtt:
- image: koenkk/zigbee2mqtt:2.6.3
+ image: koenkk/zigbee2mqtt:2.13.0
restart: unless-stopped
바꾸기 전에 백업을 먼저 하고, 바꾼 뒤에 새 이미지를 받아 다시 올린다.
cd /opt/zigbee2mqtt
tar czf ~/z2m-backup.tar.gz data docker-compose.yml
docker compose pull zigbee2mqtt
docker compose up -d zigbee2mqtt
백업은 data 디렉터리를 통째로 복사해 둔다. 지그비 망 정보가 든 database.db가 그 안에 있어서, 이 디렉터리만 있으면 태그를 되돌려 원래 상태로 갈 수 있다. 마이너 버전을 일곱 개 건너뛰지만 중간을 거칠 필요는 없다. 설정 파일 형식이 바뀌는 것도 프로그램이 한 단계씩 알아서 하고, 우리 경우 바뀌기 직전 상태가 configuration_backup_v4.yaml로 남아 있었다.
올리고 나니 목록이 제자리를 찾았다. 르그랑과 삼성과 필립스가 사라지고 shellyWS90Wind와 shellyWS90UV와 shellyWS90Rain이 그 자리에 나타났다. z2m이 내보내는 값도 6개에서 22개로 늘었다.
16시 35분에 새 버전이 올라왔다. 대시보드를 새로고침했다. 항목은 스물두 개로 늘어 있었고, 값은 스물두 개가 전부 null이었다. 채워진 것은 z2m이 조도를 보고 짐작하는 날씨 상태 하나뿐이었다. 태그를 올리는 것으로 끝인 줄 알았는데 아니었다.
여기에는 지그비 기기가 값을 보내는 방식이 얽혀 있다. 기기는 가진 값을 아무 때나 쏟아 내지 않고, 어떤 속성을 얼마 간격으로 보고할지는 페어링할 때 코디네이터가 미리 요청해 둔다. 우리 WS90이 페어링된 시점에는 커스텀 클러스터를 아는 정의가 없었으니 그런 요청도 없었다. 받는 쪽 사전만 새것으로 바꾼다고 해서, 말해 달라고 청한 적 없는 단어가 들려올 리는 없다.
그러니 보고를 다시 요청해야 한다. Zigbee2MQTT 기기 화면의 Reconfigure 버튼이 그 일을 하고, MQTT로도 같은 요청을 보낼 수 있다.
mosquitto_pub -t zigbee2mqtt/bridge/request/device/configure -m '{"id":"ws90"}'
# 응답: zigbee2mqtt/bridge/response/device/configure {"status":"ok"}
16시 41분에 Reconfigure를 눌렀더니 1분 45초 뒤 디버그 로그에 기다리던 줄이 찍혔다.
Received Zigbee message from 'ws90', type 'attributeReport',
cluster 'shellyWS90Wind', data '{"windDirection":1190}' from endpoint 1
ZCL command ... shellyWS90Wind.defaultRsp(... manufacturerCode:5264 ...)
5264는 0x1490, 문서에 적힌 셀리 코드 그대로다. 1190은 119.0도이고, 10배로 실어 보낸 것을 해석기가 되돌린다. 배터리로 도는 기기지만 재페어링은 필요 없었다. 이 기기는 초 단위로 보고하느라 늘 깨어 있어서 부탁을 바로 받는다.
고치고 나서 무엇이 오는지 한 번 세어 봤다. 스물두 개 가운데 열네 개는 기기 쪽에서 오고, 여덟 개는 z2m이 그 값들로 계산해 얹는다. 열네 개 중 하나인 배터리 전압은 우리 기기에서 늘 비어 있고, 신호 품질은 기기가 잰 값이 아니라 무선이 프레임에 붙이는 꼬리표다. 아래 표의 마지막 열은 이 글을 쓰는 동안 실제로 읽은 값이다.
| 값 | 뜻 | 지금 |
|---|---|---|
| 표준 클러스터. 어느 지그비 허브에서나 읽는다 | ||
temperature | 기온 | 25.0 °C |
humidity | 상대습도 | 89 % |
pressure | 기압. z2m은 kPa로 준다 | 998 hPa |
illuminance | 조도. 일사량(W/m²)이 아니라 럭스다 | 1 lx |
battery | 배터리 잔량 | 99 % |
voltage | 배터리 전압. 우리 기기에서는 늘 비어 있다 | 안 옴 |
linkquality | 신호 품질. 값이 아니라 무선이 붙이는 꼬리표다 | 0 |
| 0xFC01. 셀리 전용, 바람 | ||
wind_speed | 풍속 | 0.0 m/s |
gust_speed | 돌풍. 짧은 구간의 최댓값 | 0.0 m/s |
wind_direction | 풍향. 0이 북, 90이 동 | 277° |
| 0xFC02. 셀리 전용, 자외선 | ||
uv_index | UV 지수 | 0 |
| 0xFC03. 셀리 전용, 비 | ||
rain_status | 지금 비가 오는가 | 비 아님 |
precipitation | 누적 강수량 | 0.0 mm |
| 셀리 전용, 전원 | ||
capacitor_voltage | 태양광 축전 전압. 낮에 차오르고 밤에 배터리를 아낀다 | 1.3 V |
| z2m이 계산해서 얹는 값. 2.9.0부터 | ||
dew_point | 이슬점 | 23.1 °C |
apparent_temperature | 체감온도 | 35.4 °C |
humidex | 습도를 반영한 더위 지수 | 35.4 °C |
wind_chill | 바람을 반영한 추위 지수 | 25.0 °C |
heat_stress | 열 스트레스 | 66 % |
rain_rate | 강우 강도 | 0.0 mm/h |
pressure_trend | 기압이 오르내리는 속도 | 0.0 hPa/h |
weather_condition | 날씨 상태를 한 낱말로 | clear-night |
이슬점부터 아래 여덟 줄은 계산값이라는 것을 알고 쓰는 편이 낫다. 기기가 잰 것이 아니라 기온과 습도와 바람으로 유도한 값이라, 원본이 틀리면 여덟 개가 같이 틀린다. 대신 이 값들은 z2m이 계산하므로 대시보드마다 다시 만들 필요가 없다. 우리 대시보드에서 이슬점을 구하던 Magnus 식은 이번에 지웠다.
고친 뒤로 쌓인 여섯 시간이다. 아래 여섯 칸은 시간축을 함께 쓰고, 어느 칸에든 마우스를 올리면 같은 시각의 값이 여섯 개 함께 나온다.
이 여섯 칸 중에 기기가 직접 잰 값은 조도, 기온, 습도, 풍속, 돌풍, 축전 전압이고 이슬점과 열 스트레스는 z2m이 계산해 얹은 값이다. 둘을 한 화면에 두면 계산값이 원본을 어떻게 따라가는지가 보인다. 기온이 내려가는데 습도가 올라가니 이슬점은 거의 평평하고, 열 스트레스는 그 둘을 합쳐 한 방향으로 내려간다.
값이 모자랄 때 확인할 것은 두 줄이다. 내보내는 값 목록에 찾는 것이 없고 클러스터 목록에 엉뚱한 회사 이름이 보이면 무선 문제가 아니라 번역 문제다. 항목은 있는데 값만 비어 있으면 보고를 다시 부탁해야 하는 문제다. 이 두 줄을 확인하지 못한 탓에 필요하지도 않은 게이트웨이를 한참 검색했다.
한계도 있다. 일사량은 지그비로 오지 않고, 조도가 럭스 단위로 오는 데까지다. 기기 펌웨어를 Zigbee2MQTT로 갱신하는 경로도 없다(supports_ota가 false다). 다만 이번 일에서 낡은 쪽은 기기가 아니었으니 그건 문제가 되지 않았다.
맨 처음에 만들었던 표로 돌아가면, 오는 값 아래에 안 오는 것이라고 적고 풍속, 풍향, 강수, UV를 넣어 둔 칸이 있었다. 그 칸은 지웠다. 거기 적혀 있던 네 개는 그때도 오고 있었고, 못 받고 있던 쪽은 기기가 아니었다.
정확한 이름은 Ecowitt WS90 Powered by Shelly, 짧은 모델명은 SBWS-90CM이다(Shelly 기술문서). 같은 이름의 원래 Ecowitt WS90은 868/915MHz 전용 게이트웨이를 쓰는 물건이라, 겉모습만 거의 같을 뿐 속이 다르다. 이쪽에는 지그비와 블루투스가 들어 있다. 살 때 헷갈리기 쉬운 지점이다.
| 조작 | 동작 |
|---|---|
| CAL 한 번 | 블루투스 페어링 (30초) |
| CAL 두 번 | 지그비 연결 시작 (3회 재시도) |
| CAL 세 번 | 하트비트 LED 켜고 끄기, 지그비 Identify 질의 방송 |
| CAL 길게 | 캘리브레이션 시작 |
| RST 한 번 | 리셋 |
출처는 Shelly 기술문서다. 연결 대기 상태는 3분간 열리고, 연결에 성공하면 파란 LED가 한 번 깜빡인다(Zigbee2MQTT 기기 페이지). 평소 8.8초마다 깜빡이는 것은 데이터 전송 표시라 페어링 신호와 헷갈리기 쉽다. CAL 두 번에 반응이 없으면 RST를 한 번 눌러 재부팅하고 20초쯤 기다린 다음 다시 시도한다.