샤오미 온습도계에서 159일 기록 꺼내 웹으로 보기
이 글과 연결된 글 보기
드래그로 이동 · 휠/두 손가락으로 확대 · 방향키로 글 선택 · 0으로 맞춤. 실선: 본문 링크, 점선: 시리즈·주제의 대표 연결.표시된 글 11편 목록
외출할 때 들고 다닐 샤오미 온습도계의 데이터를 계속 모아 보는 작업을 시작했습니다. 시작은 펌웨어 교체였지만, 정작 결과를 만든 것은 기기에 남아 있던 순정 기록을 읽는 일이었습니다. 시간별 온습도 기록 3,822개, 첫 기록부터 마지막 기록까지 약 159일 구간을 회수해 CSV와 누적 저장 원장으로 보존하고, 로그인으로 보호되는 DataPopcorn 전용 웹에 올렸습니다.
현재 완료한 것은 기존 기록 회수와 웹 조회입니다. 집에 돌아오면 자동으로 업로드하는 기능은 별도 ESP32 수신기를 연결할 다음 단계로 남겨 두었습니다.

실제 구축 화면입니다. ‘수신기 연결 전’이라는 표시처럼, 웹 배포와 센서 자동 수집을 구분했습니다.
이 글은 이런 분께 필요합니다
- 샤오미 블루투스 온습도계에서 표시값뿐 아니라 저장 기록도 꺼내 보고 싶은 분
- CSV를 계속 모으되 중복이나 시간대 오류를 피하고 싶은 분
- 센서 데이터를 개인 웹에서 보고, 나중에 ESP32 자동 수집까지 이어가려는 분
이번에 실제로 확인한 기기는 LYWSD03MMC, 하드웨어 B1.6, 연결 후 보고된 펌웨어 2.1.1_0159입니다. Mac과 Web Bluetooth 지원 브라우저, Mi Home에 등록된 계정을 사용했습니다. 같은 제품명이라도 하드웨어·펌웨어가 다를 수 있으므로 이 글을 모든 샤오미 온습도계에 그대로 적용하지 마세요.
중간 원본과 ATC 펌웨어의 전송 완료 메시지는 확인했지만, 후속 연결에서 기기는 순정 버전을 보고했습니다. ATC가 정상 적용됐다고 결론 내리지 않았고, 추가 쓰기를 중단한 뒤 저장 기록을 읽었습니다. 아래의 성공 경로는 이력 회수·검증·웹 조회입니다.
Step 1. 기기를 연결하고 실제 버전을 읽습니다
Telink BLE OTA Flasher를 열고 Connect를 누른 뒤 온습도계를 선택합니다. 이 단계에서는 펌웨어 파일을 고르거나 Start Flashing을 누르지 않습니다.
이번 연결에서는 하드웨어 B1.6, 소프트웨어 0159, 펌웨어 2.1.1_0159가 보고됐습니다. 당시 수신값은 22.66°C, 습도 41%, 배터리 전압 2901mV였습니다. 전압을 읽었다고 배터리 잔량이나 수명을 검증한 것은 아닙니다.

이 과정에서는 자동화 도구가 Connect를 누르면 기기 선택창이 취소됐고, 사용자가 직접 선택하자 연결됐습니다. 이는 이번 실행 환경에서 관찰한 현상이지, 모든 브라우저에서 자동 연결이 불가능하다는 뜻은 아닙니다.
선택창의 **‘페어링됨’**도 오류 표시로 해석하면 안 됩니다. 이전에 이 사이트에서 연결을 허용했던 기기일 수 있습니다. 실제 연결 성공과 펌웨어 버전은 연결 후의 응답으로 확인해야 합니다.
Step 2. Mi Home 등록과 웹 로그인 계정을 맞춥니다
기기 인증 화면에서 ID·Token·Bind Key를 요구해 Mi Home 등록 상태부터 확인했습니다. 처음 QR 로그인을 승인한 웹 계정은 Mi Home 앱 계정과 달랐습니다. 올바른 계정으로 바꾼 뒤에도 온습도계가 없어, 앱에서 기기를 새로 등록했습니다.
등록 중 ‘기기를 찾을 수 없음’이 나왔을 때는 Mac의 기존 BLE 연결을 Disconnect로 끊은 뒤 다시 검색했습니다. 그 뒤 등록에 성공했습니다. 기존 연결이 영향을 줬을 가능성은 있지만, 검색 실패의 원인이 언제나 이것이라고 단정할 수는 없습니다.
같은 상황이면 다음 순서로 확인합니다.
- Mi Home 앱의 계정과 QR을 승인할 휴대폰 웹 계정이 같은지 확인합니다.
- 그 계정의 홈 화면에 온습도계가 등록돼 있는지 확인합니다.
- 등록 중에는 다른 브라우저·앱의 연결을 끊고 다시 검색합니다.
- 등록을 마쳤으면 앱의 연결을 종료하고 Mac에서 다시 연결합니다.
Step 3. QR 로그인으로 해당 기기의 인증 키를 확보합니다
공식 Xiaomi-cloud-tokens-extractor 저장소의 안내에 따라 도구를 준비했습니다. Mac에서는 격리된 Python 가상환경에 의존성을 설치해 실행했고, 로그인 방식으로 QR을 골랐습니다.
이번에는 Mi Home 앱 스캔이 되지 않아 일반 카메라로 QR을 열고 휴대폰 웹에서 로그인했습니다. 조회한 QR 응답의 유효시간은 300초였습니다. 계정 전환을 끝내 놓고 새 QR을 발급하는 편이 낫습니다. QR 이미지가 새것이어도 대기 프로그램이 종료돼 있으면 인증을 마칠 수 없습니다.
등록 후 다시 조회하니 miaomiaoce.sensor_ht.t2 모델이 발견됐고, Token과 BLE Key가 모두 존재했습니다. 설치 페이지의 Device known id / Mi Token / Mi Bind Key에 해당 값을 넣어 Login successful을 확인했습니다.
도구의 기본 터미널 출력에는 키가 포함될 수 있습니다. 이번에는 출력 코드를 수정해 키를 가렸고, 결과 파일은 권한 600으로 저장했습니다. 이 글과 다운로드 파일에는 실제 기기 ID·Token·Bind Key·로그인 QR을 넣지 않았습니다. 화면 공유나 로그 수집 도구에도 원문을 남기지 마세요.
Step 4. 순정 펌웨어의 저장 기록을 읽습니다
초기 안내에서 ‘순정은 기록이 없다’고 설명한 것은 잘못됐습니다. Flasher에 연결된 순정 LYWSD03MMC 기록 조회 페이지의 소스를 확인하니, 순정 기기의 저장 이력을 읽는 기능이 있었습니다.
이번에는 이미 인증된 브라우저의 GATT 연결을 재사용했습니다. 아래는 실제 조회에 사용한 특성입니다. UUID의 공통 뒷부분은 -7a0a-4b0c-8a1a-6ff2997da3a6입니다.
| 특성 앞부분 | 이번에 읽거나 설정한 내용 |
|---|---|
ebe0ccb9 | 기록 인덱스 정보. 8바이트 응답에서 5614와 3822를 받았습니다. |
ebe0ccb7 | 기기의 시간 카운터. 달력 시각 추정 기준으로 보존했습니다. |
ebe0ccbc | 저장 기록 알림을 구독했습니다. |
ebe0ccba | 조회 시작 위치에 0을 써서 보관 중인 기록을 처음부터 요청했습니다. |
마지막 항목의 쓰기는 조회 시작 위치 설정입니다. 기록 삭제 명령은 사용하지 않았습니다. 새 웹 페이지로 옮기면 브라우저의 기기 선택 권한도 다시 필요할 수 있으므로, 이번 회수는 인증된 연결 안에서 진행했습니다.
첫 레코드의 인덱스는 1793, 기기 카운터는 6,458,400초였습니다. 온도 최고 23.9°C / 최저 23.7°C, 습도 최고 34% / 최저 33%를 받았습니다. 마지막 인덱스 5614까지 읽어 예상한 3,822개 전체가 들어왔는지 대조했습니다.
이 데이터가 의미하는 것
각 행은 시간 구간의 최고·최저 온도와 습도입니다. 1분 순간값도 아니고, 시간 평균도 아닙니다. 최고·최저의 중간값을 계산하더라도 실제 평균 측정값이라고 부를 수 없습니다.
또한 이 기록이 전부 외출 중의 값이라는 근거는 없습니다. 위치나 외출 구분이 없으므로, 보관 중 실내 기록과 외출 기록을 나누려면 별도 활동 시간이 필요합니다.
Step 5. 개수·간격·시간 해석을 검증합니다
이번에 회수한 원본을 검사한 결과입니다.
| 검사 | 실제 결과 |
|---|---|
| 기록 수 | 3,822개 |
| 인덱스 | 1793~5614, 누락·중복 0개 |
| 연속 카운터 차이 | 3,600초가 3,821회 |
| 첫~마지막 카운터 사이 | 약 159.21일 |
| 전체 기록의 온도 최저~최고 | 18.2~33.7°C |
| 전체 기록의 습도 최저~최고 | 20~76% |
| CSV 행 수 | 헤더 포함 3,823행 |
약 159일은 카운터상 첫 기록부터 마지막 기록까지의 구간입니다. 원래 측정된 모든 순간값을 빠짐없이 회수했다는 뜻은 아닙니다.
달력 시각은 다음처럼 추정했습니다.
추정 기록 시각 = 현재 조회 시각
- 현재 기기 카운터
+ 레코드의 기기 카운터
조회 시각과 두 카운터의 단위를 맞춰 계산합니다. 기기 시계의 오차나 과거 재설정 이력은 확인하지 못했으므로, 결과 열 이름을 estimated_time_utc로 두고 원시 device_seconds도 보존했습니다. 웹에서 한국 시간으로 보여 주더라도 ‘추정 시각’이라는 성격은 바뀌지 않습니다.
ATC용 GraphMemo CSV는 별도 형식입니다. 그 페이지의 Set Time이 현지 시각을 반영한 초 숫자를 쓰는 것도 소스에서 확인했지만, 이번에 실제로 회수한 순정 이력과 혼용하지 않았습니다.
Step 6. 원본·CSV·SQLite 누적 원장을 함께 보존합니다
회수 직후 원본 JSON을 저장하고 SHA-256을 남겼습니다. 그 다음 최고·최저값과 원시 시간을 유지한 CSV·JSONL을 내보냈습니다. 반복 회수를 위해 SQLite에는 기기 ID·기록 인덱스·기기 카운터를 묶어 중복 기준으로 사용했습니다.
아래 패키지는 이번 실제 원본에 실행해 확인한 내보내기·누적 저장 코드입니다. 센서에 연결하는 프로그램은 포함하지 않습니다. 위 단계에서 회수한 완료 상태의 원본 JSON을 입력으로 받습니다. 개인정보 보호를 위해 실제 159일 원본 전체도 공개 패키지에 넣지 않았습니다.
압축을 푼 폴더에서 Node.js로 실행합니다. 이번에는 Node.js 26.5.0에서 실행했습니다.
node export-original.mjs history-raw.json ./data-original
node accumulate-original.mjs history-raw.json sensor-01 ./data-original
export-original.mjs는 회수 완료 여부, 예상 개수, 인덱스 누락과 최고·최저 범위를 검사한 뒤 history.csv, history.jsonl, summary.json을 만듭니다. accumulate-original.mjs는 original.sqlite와 전체 누적 all-history.csv를 관리합니다.
같은 실제 원본을 두 번 적재한 결과, 첫 실행은 3,822개 추가, 두 번째는 0개 추가·3,822개 중복으로 처리됐습니다. 같은 식별값에 다른 수치가 들어오면 덮어쓰지 않고 중단하도록 했습니다.
배터리 교체 등으로 카운터와 인덱스가 모두 재사용되는 경우까지 구분된다는 보장은 없습니다. 그런 재설정이 확인되면 sensor-01-epoch2처럼 수집 구간의 식별자를 나눠야 합니다.
Step 7. 개인 웹에서 보되, 오래된 값을 현재값처럼 표시하지 않습니다
DataPopcorn의 기존 로그인 보호 허브에 별도 환경 기록 화면을 만들고 실제 3,822개를 반영했습니다. 새 유료 서비스나 DNS 변경 없이 기존 실행 서버와 SQLite를 사용했습니다.
- 7일 / 30일 / 전체 기간별 온도·습도 그래프
- 최근 저장 구간, 마지막 동기화 시각, CSV 다운로드
- 소유자 계정만 조회하는 접근 제한
- 기록이 없거나 수신기가 연결되지 않았을 때의 상태 표시
- 화면이 열려 있을 때 30초 간격 조회, 숨겨지면 조회 중단
화면의 30초 갱신은 센서의 수집 주기가 아닙니다. 현재 화면의 데이터는 한 번 회수한 시간별 기록이며, 자동 수집기는 아직 연결하지 않았습니다. 서버에 파일을 적재한 시각과 실제 BLE 회수 완료 시각도 구분했습니다.
전체기간 차트를 검수하다가 단순 간격 샘플링이 최고·최저를 놓치는 문제도 발견했습니다. 구간별로 묶을 때 high=max, low=min을 쓰도록 바꿔, 화면에서도 18.233.7°C와 2076%가 보존되게 했습니다. 구간 평균을 만든 것은 아닙니다.
서버에서는 실제 자료 적재·중복·충돌 롤백·기간 조회·권한 차단·CSV 검사 16개 assertion을 통과했습니다. 별도로 공개 주소에서 로그인이 없으면 인증 화면으로 이동하는 것, 로그인한 소유자에게 실제 자료가 보이는 것, 모바일 화면도 확인했습니다. 이 검증은 귀가 후 자동 수집이 작동한다는 검증과는 별개입니다.
Step 8. 귀가 후 자동 동기화는 별도 ESP32로 이어갑니다
원하는 최종 동작은 단순합니다. 밖에서는 온습도계가 기록하고, 집으로 돌아오면 가까운 수신기가 저장 기록을 회수해 웹에 합치는 방식입니다.
Cardputer를 수신기로 겸용하면 기존 장비를 활용할 수 있습니다. 다만 이미 주문 벨로 쓰고 있고 다른 하드웨어 실험에도 활용하려는 장비여서, 사용자가 별도 ESP32도 가지고 있다고 알려주어, 집에 고정할 전용 수집기의 후보로 검토 중입니다.
현재 별도 ESP32를 가지고 있다는 것까지만 확인했습니다. 보드 모델·블루투스 지원·설치 위치·자동 회수 동작은 아직 확인하지 않았고, 펌웨어도 올리지 않았습니다. 따라서 이 글의 완료 범위는 아래와 같습니다.
| 단계 | 현재 상태 |
|---|---|
| 기기 연결·인증 | 확인 |
| 저장 기록 3,822개 회수 | 완료 |
| CSV·SQLite 누적 저장 | 완료 |
| 로그인 보호 웹 조회 | 배포·실제 표시 확인 |
| 1분 단위 기록 | 미설정 |
| ESP32 귀가 후 자동 동기화 | 다음 단계 |
| GPS·활동 기록 결합 | 미구현 |
시행착오: OTA ‘전송 완료’는 적용 완료가 아니었습니다
실험 중에는 개발자 안내를 따라 중간 원본 Original_OTA_Xiaomi_LYWSD03MMC_v2.1.1_0159c.bin과 ATC_v59.bin도 전송했습니다. 각각 약 119.671초, 95.132초 뒤 Update done을 확인했습니다.

하지만 후속 연결에서 같은 기기 키로 인증에 성공한 센서는 2.1.1_0159를 보고했습니다. 새 펌웨어로 부팅했다고 확인하지 못했고, 원인도 확정하지 못했습니다. 같은 파일을 계속 쓰는 대신, 원래 목적이었던 저장 기록 회수로 방향을 바꿨습니다.
따라서 이 글의 파일명을 그대로 따라 펌웨어를 올리는 것은 권하지 않습니다. 먼저 실물의 버전과 순정 기록 조회 가능 여부를 확인하세요. 초기 안내에 있던 ‘언제든 원복 가능’, ‘배터리 1년 이상’ 역시 이 실물에서 검증한 보장이 아닙니다.
오류가 생기면 이 순서로 확인합니다
| 증상 | 이번에 확인한 대응과 한계 |
|---|---|
| QR 만료 | 계정 전환을 먼저 마치고 새 QR을 발급했습니다. 대기 프로그램이 살아 있는지도 확인했습니다. |
| 계정에 온습도계가 없음 | 앱과 웹 로그인 계정을 맞추고 기기를 등록했습니다. |
| Mi Home에서 기기를 못 찾음 | Mac 연결을 끊고 재검색한 뒤 등록에 성공했습니다. 모든 경우의 원인으로 단정하지는 않습니다. |
| ‘페어링됨’인데 연결이 안 됨 | 페어링 표시와 실제 GATT 연결 성공을 구분했습니다. |
| 범위 오류·GATT 오류 | 즉시 고장으로 판단하지 않고 기존 연결을 정리하고 다시 선택했습니다. |
| Mac 직접 BLE 검색 실패 | 에이전트 프로세스에서 Bluetooth is unsupported가 나왔습니다. Mac 하드웨어 자체의 미지원으로 일반화하지 않았습니다. |
| 전송 완료인데 버전이 그대로임 | 실제 버전을 다시 읽고 적용 성공 주장을 철회했습니다. |
| 그래프의 전체 최고값이 원본과 다름 | 간격 샘플링 대신 구간별 최고·최저를 보존했습니다. |
이번 작업에서 가장 중요한 확인은 펌웨어 기능 목록보다 실제 기기에서 읽힌 데이터였습니다. 다음에는 별도 ESP32 모델을 확인하고, 귀가 후 자동 회수와 업로드를 실제로 검증한 뒤 후속 기록으로 이어가겠습니다.
