Ecowitt WS90, Zigbee2MQTT

지그비 기상 센서에서 바람과 UV와 강우 값이 오지 않았다

낡은 쪽은 기기가 아니라 그 값을 받아 적는 소프트웨어였다.

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분마다 긁어 가고, 그래프는 그 위에서 그려진다.

무선 WS90야외, 배터리와 태양광엔드 디바이스 스마트 플러그상시 전원라우터로 중계 SLZB-MR3U코디네이터망을 여는 단 하나 지그비 지그비 이더넷 소프트웨어. 아래 네 칸은 한 컨테이너 호스트 안에서 돈다 Zigbee2MQTT지그비를 MQTT로 MosquittoMQTT 브로커 익스포터MQTT를 메트릭으로 VictoriaMetrics저장과 Grafana 문제가 난 자리
그림 1. 야외 센서에서 그래프까지. 이 글에서 문제가 난 곳은 왼쪽의 무선 구간이 아니라 아래 첫 칸, 지그비를 MQTT로 옮기는 자리다.

이 그림에서 미리 짚어 둘 것이 하나 있다. 값이 모자란 원인을 찾을 때 사람은 대개 왼쪽 끝, 그러니까 센서와 전파를 먼저 의심한다. 그런데 아래 첫 칸이 지그비의 말을 MQTT로 옮기는 번역 단계이고, 여기가 틀리면 무선이 아무리 멀쩡해도 값은 오지 않는다.

참고로 무선 쪽이 멀쩡했다는 근거는 나중에 따로 확인했다. 페이로드에 실려 오는 신호 품질 값이 255 만점에 15로 낮아 센서가 멀어서인가 싶었는데, 망 지도를 확인해 보니 WS90에서 라우터까지는 117로 넉넉했고 낮은 값은 라우터에서 코디네이터로 들어오는 마지막 구간에서 나왔다. 손을 대야 할 곳이 있다면 센서 쪽이 아니라 그 반대편이라는 뜻이고, 어느 쪽이든 이 글의 원인과는 무관했다.

클러스터 목록의 이상한 세 줄

Zigbee2MQTT는 zigbee2mqtt/bridge/devices 토픽에 기기 정보를 그대로 실어 보낸다. 그 기기가 무슨 기능을 갖췄는지도 함께 들어 있다. 꺼내 보다가 세 줄에서 눈이 멈췄다.

msTemperatureMeasurement, msPressureMeasurement, msRelativeHumidity,
manuSpecificLegrandDevices,        (르그랑?)
manuSpecificSamsungAccelerometer,  (삼성?)
manuSpecificPhilips2               (필립스?)

기상 센서 안에 프랑스 전기설비 회사와 한국 가전 회사와 네덜란드 조명 회사가 동시에 들어 있을 리 없다. 이게 왜 오역인지 알려면 지그비를 조금 알아야 하는데, 두 문단이면 된다.

지그비에서는 값의 의미가 클러스터라는 단위로 정해져 있다. 온도는 온도 클러스터, 습도는 습도 클러스터다. 표준에 없는 값을 보내고 싶은 회사는 제조사 전용으로 열린 번호대를 쓴다. 문제는 그 번호대를 회사끼리 나눠 갖지 않는다는 데 있다. 같은 번호를 르그랑도 쓰고 삼성도 쓰고 셀리도 쓴다. 누구 것인지는 무선 프레임에 같이 실려 오는 제조사 코드로만 갈린다.

그러니까 저 세 줄은, 셀리의 번호를 만난 번역기가 그게 셀리인 줄 모른 채 철자가 같은 다른 회사의 단어를 사전에서 집어 온 결과다. 진짜 정체는 0xFC01, 0xFC02, 0xFC03이고, 각각에 무엇이 담겼는지는 Shelly 기술문서에 적혀 있다.

클러스터담는 값
0xFC01풍속, 풍향, 돌풍
0xFC02UV 지수
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.02025-12-01PR #10752. 바람과 UV와 강우 노출
2.9.02026-03-01PR #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로 남아 있었다.

올리고 나니 목록이 제자리를 찾았다. 르그랑과 삼성과 필립스가 사라지고 shellyWS90WindshellyWS90UVshellyWS90Rain이 그 자리에 나타났다. 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 ...)
그림 2. 값마다 처음 기록된 시각. 위쪽 여섯 줄은 원래부터 오던 것이고, 아래 열네 줄이 한꺼번에 나타났다. 16시 45분이라는 시각에는 Reconfigure만이 아니라 늘어난 항목을 내보내도록 수집기를 다시 올린 일도 함께 걸려 있다. 기기와 z2m을 고쳤다고 끝이 아니라 그 뒤 파이프라인도 같이 손봐야 값이 저장된다.

52640x1490, 문서에 적힌 셀리 코드 그대로다. 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_indexUV 지수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 식은 이번에 지웠다.

관측 그래프

고친 뒤로 쌓인 여섯 시간이다. 아래 여섯 칸은 시간축을 함께 쓰고, 어느 칸에든 마우스를 올리면 같은 시각의 값이 여섯 개 함께 나온다.

그림 3. 해가 지는 동안의 여섯 가지. 조도가 2만 럭스대에서 1럭스까지 떨어지는 사이 습도는 70퍼센트에서 88퍼센트로 올라가고, 기온과 이슬점의 간격이 좁혀진다. 태양광 축전 전압은 해가 남아 있는 동안 0.9볼트에서 1.3볼트까지 차오른 뒤 그대로 머문다. 조도만 로그 축이다.

이 여섯 칸 중에 기기가 직접 잰 값은 조도, 기온, 습도, 풍속, 돌풍, 축전 전압이고 이슬점과 열 스트레스는 z2m이 계산해 얹은 값이다. 둘을 한 화면에 두면 계산값이 원본을 어떻게 따라가는지가 보인다. 기온이 내려가는데 습도가 올라가니 이슬점은 거의 평평하고, 열 스트레스는 그 둘을 합쳐 한 방향으로 내려간다.

그림 4. 풍향 분포. 관측을 16방위로 나눠 센 것이고, 부채꼴이 길수록 그 방향으로 자주 불었다. 풍속이 초속 1미터를 거의 넘지 않는 저녁이라 방향도 한쪽으로 몰리지 않는다.

판정 순서와 남은 한계

값이 모자랄 때 확인할 것은 두 줄이다. 내보내는 값 목록에 찾는 것이 없고 클러스터 목록에 엉뚱한 회사 이름이 보이면 무선 문제가 아니라 번역 문제다. 항목은 있는데 값만 비어 있으면 보고를 다시 부탁해야 하는 문제다. 이 두 줄을 확인하지 못한 탓에 필요하지도 않은 게이트웨이를 한참 검색했다.

한계도 있다. 일사량은 지그비로 오지 않고, 조도가 럭스 단위로 오는 데까지다. 기기 펌웨어를 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초쯤 기다린 다음 다시 시도한다.