삼성_에어모니터_로컬MQTT_연동_가이드

삼성 에어모니터 (ACM-B1M0S) — SmartThings API 없이 로컬 MQTT로 센서값 읽기

이 문서는 삼성 “에어모니터 스탠다드”(모델명 ACM-B1M0S)를 SmartThings 클라우드를 거치지 않고, 집 안 로컬 네트워크에서 직접 센서 데이터를 받아 Home Assistant 등에 연동한 과정을 정리한 가이드입니다. 이 기기는 Qingping(칭핑) 하드웨어를 삼성이 리브랜딩한 제품으로, 내부적으로는 여전히 Qingping의 클라우드 인프라(cleargrass.com)를 바라보고 있습니다. 그 통신을 우리가 만든 로컬 MQTT 브로커로 가로채는 방식입니다.

⚠️ 주의사항

  • 이 작업은 기기 제조사의 정식 지원 기능이 아니며, ADB 개발자 모드 활성화와 시스템 파일
    수정을 포함합니다. 진행 전 반드시 본인 소유 기기에서만 시도하세요.
  • 기기 소프트웨어 버전에 따라 세부 동작(도메인명, 토픽명, 로그 포맷 등)이 다를 수 있습니다.
    이 문서는 특정 개체(예시 기기 ID)를 기준으로 작성되었으므로, 각자 자신의 기기에서 나온
    값으로 치환해서 사용해야 합니다.
  • 무선 신호나 클라우드 인증 서버로 나가는 경로를 임의로 바꾸는 작업이므로, 잘못 설정하면
    기기가 정상적으로 인터넷 기능(펌웨어 업데이트, 날씨 표시 등)을 못 쓸 수 있습니다.
    (센서 자체는 로컬에서 계속 정상 동작합니다.)

목차

  1. 사전 준비물
  2. 기기에서 ADB 개발자 모드 켜기
  3. ADB로 root shell 접속
  4. 기기 소프트웨어 구조 파악하기
  5. 로그에서 기기 ID(Device ID) 알아내기
  6. 로그에서 실제 클라우드 도메인 알아내기
  7. 네트워크 격리 설정 (TTL 트릭)
  8. hosts 파일로 로컬 리다이렉트
  9. 로컬 MQTT 브로커(Mosquitto) 준비
  10. 인터넷 연결성 체크 우회하기 (더미 HTTP 서버)
  11. 재부팅 후 연결 확인
  12. 수신되는 MQTT 페이로드 구조
  13. Home Assistant 연동 (mqtt.yaml)
  14. 고급: 명령 토픽으로 폴링 주기 단축하기
  15. HAOS Mosquitto 애드온에서 기존 브로커로 계정 추가하기
  16. 트러블슈팅
  17. 전체 명령어 요약

1. 사전 준비물

항목설명
기기삼성 에어모니터 (모델명 ACM-B1M0S, 다른 Qingping 계열 모델도 유사)
PCWindows/Mac/Linux, USB-C 케이블
ADB (Android Debug Bridge)platform-tools 패키지 설치
Home Assistant OS (HAOS)Mosquitto broker 애드온이 이미 설치되어 실행 중이어야 함
HTTP 응답용 서버 1대 (HAOS와 같은 머신이어도 무방)인터넷 연결성 체크 우회용 더미 웹서버 구동
기기가 접속된 Wi-Fi 네트워크 정보기기와 같은 서브넷에 있어야 함

이 가이드의 예시에서는:

  • 기기 IP: 192.168.219.16
  • HAOS(Mosquitto 애드온 실행 중) IP: 192.168.219.130

각자 환경에 맞는 IP로 바꿔서 진행하세요.


2. 기기에서 ADB 개발자 모드 켜기

  1. 에어모니터 화면에서 설정 > About(정보) 로 이동
  2. 화면의 Device Name(기기 이름) 항목을 5번 연속 탭
  3. “Developer Options(개발자 옵션)”가 열리면 진입
  4. Debug ModeAdbd Debugging 항목을 켜기
  5. USB-C 케이블로 PC와 연결

이 트릭은 안드로이드 계열 기기에서 흔히 쓰이는 방식으로, Qingping/삼성 리브랜딩 모델 다수에서 동일하게 동작하는 것으로 확인되었습니다.


3. ADB로 root shell 접속

PC에서 platform-tools 폴더로 이동 후:

adb devices        # 기기가 목록에 뜨는지 확인
adb shell

접속되면 아래와 같은 배너가 보입니다:

BusyBox v1.27.2 () built-in shell (ash)
Tina Linux (Neptune, ...)
Tina Linux is Based on OpenWrt!

이 기기는 TinaLinux(OpenWrt 기반의 Allwinner SoC용 경량 리눅스)로 동작하며, 셸에 이미 root 권한으로 접속됩니다 (별도 su 불필요).


4. 기기 소프트웨어 구조 파악하기

핵심 프로세스 구조를 먼저 확인합니다.

ps -w

주요 프로세스:

프로세스역할
/data/bin/SansaApp메인 애플리케이션 (Qt 기반, 센서 수집·클라우드 통신·MQTT 담당)
/data/bin/watchdog.shSansaApp이 죽으면 자동 재시작하는 감시 스크립트

원래 Qingping 순정 기기는 QingSnow2App이라는 프로세스를 쓰지만, 삼성 리브랜딩 버전은 **SansaApp**이라는 자체(Qingping OEM) 앱으로 교체되어 있습니다. 부팅 로고 등 일부 리소스에만 qingping 흔적이 남아 있는 정도입니다.

설정 저장 방식도 확인해두면 좋습니다. Qingping 오픈소스 프로젝트들에서 흔히 언급되는 /data/etc/settings.ini 방식이 아니라, 이 기기는 SQLite DB를 사용합니다.

ls -la /data/etc/
  • 설정 DB: /data/etc/Sansa.db (테이블명 table_config, 예: MQTT_HOSTNAME,
    MQTT_PORT, MQTT_USERNAME, MQTT_PASSWORD 등의 키 보관)
  • 로그 디렉토리: /data/etc/log/YYYY-MM-DD.log (날짜별 텍스트 로그)
  • 디버그 로그: /data/etc/debug/watchdog.log

5. 로그에서 기기 ID(Device ID) 알아내기

Device ID는 이후 모든 단계(로그 필터링, MQTT 토픽 구독)에서 반드시 필요한 값입니다. 가장 쉬운 방법은 오늘 날짜 로그 파일을 열어 HTTP 요청 URL이나 MQTT 연결 로그를 보는 것입니다.

cat /data/etc/log/$(date '+%Y-%m-%d').log | grep -i "device_id\|ClientID" | head -20

로그에 아래와 같은 줄들이 보입니다:

[DEBUG][HTTP-...](URL:https://sansa.cleargrass.com/v2/sansa/device/timestamp?device_id=51D0D9713C3F11EB813200163E000886)
[DEBUG][MQTT_APP-...](ClientID:51D0D9713C3F11EB813200163E000886)

여기서 device_id= 뒤에 오는 32자리 16진수 문자열, 혹은 ClientID: 뒤에 오는 동일한 값이 바로 기기 고유 ID입니다 (예시 기기의 경우 51D0D9713C3F11EB813200163E000886).

이 값은 이후:

  • MQTT 토픽 이름(sansa/data/{device_id}, sansa/command/{device_id})
  • 클라우드 API 호출 URL의 쿼리 파라미터

에 반복해서 등장하므로, 한 번 확인해서 메모해 두면 됩니다.

참고: 이 ID는 기기 내부 저장소(SQLite DB나 별도 설정 파일)에도 저장되어 있을 가능성이 높지만, 로그에서 바로 확인하는 것이 가장 빠르고 명확합니다.


6. 로그에서 실제 클라우드 도메인 알아내기

기기가 원래 통신하려는 클라우드 서버 주소도 같은 방식으로 로그에서 확인할 수 있습니다.

cat /data/etc/log/$(date '+%Y-%m-%d').log | grep -i "hostname\|URL:" | head -30

확인된 도메인 예시 (기기/지역별로 다를 수 있음):

도메인용도
mqtt.ko.cleargrass.comMQTT 브로커 주소 (한국 리전)
sansa.cleargrass.comHTTPS API 서버 (시간 동기화, 날씨 정보 등)

만약 로그에서 바로 안 보이면, 실행 파일 내부 문자열에서도 확인 가능합니다:

strings /data/bin/SansaApp | grep -iE "cleargrass|\.com|mqtt"

7. 네트워크 격리 설정 (TTL 트릭)

기기가 클라우드로 직접 나가지 못하게, IPv4 TTL을 낮춰서 로컬 네트워크 밖으로는 패킷이 못 나가게 막습니다. (라우터까지는 도달하지만 그 다음 홉인 인터넷 게이트웨이에서 패킷이 소멸됩니다.)

cat << EOF > /etc/sysctl.conf
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
net.ipv6.conf.lo.disable_ipv6 = 1
net.ipv4.ip_default_ttl=2
EOF

재부팅 시에도 항상 적용되도록 init 스크립트로 등록합니다:

cat << EOF > /etc/init.d/S52custom
/sbin/sysctl -p /etc/sysctl.conf
EOF
chmod +x /etc/init.d/S52custom

앱이 재시작 후에도 클라우드로 재접속을 시도하지 않도록, 지금 실행 중인 앱과 감시 스크립트도 종료해둡니다 (재부팅하면 어차피 다시 뜨지만, 즉시 반영하고 싶다면):

killall watchdog.sh
killall SansaApp

8. hosts 파일로 로컬 리다이렉트

/etc/hosts에 클라우드 도메인들을 로컬 서버 IP로 매핑합니다.

cat /etc/hosts     # 현재 상태 확인

echo "192.168.219.137 mqtt.ko.cleargrass.com" >> /etc/hosts

도메인명은 6번 단계에서 확인한 본인 기기의 실제 도메인으로 바꿔서 입력하세요. 예시 기기의 경우 mqtt.ko.cleargrass.com(한국 리전)이었지만, 다른 지역/기기는 mqtt.bj.cleargrass.com 등 다른 서브도메인을 쓸 수 있습니다.

IP를 나중에 바꿔야 한다면:

sed -i '/mqtt.ko.cleargrass.com/d' /etc/hosts
echo "192.168.219.137 mqtt.ko.cleargrass.com" >> /etc/hosts

9. 로컬 MQTT 브로커(Mosquitto) 준비

로컬 서버(예: 192.168.219.137)에 Mosquitto를 설치하고 인증 없이(또는 원하면 계정 설정) 접속 가능하게 띄웁니다.

sudo apt update
sudo apt install -y mosquitto mosquitto-clients

기본 설정 (인증 없이 접속 허용, 필요에 따라 계정/비밀번호 설정 가능):

sudo tee /etc/mosquitto/conf.d/local.conf << 'EOF'
listener 1883 0.0.0.0
allow_anonymous true
EOF

sudo systemctl restart mosquitto
sudo systemctl status mosquitto

리스닝 상태 확인:

ss -tlnp | grep 1883

10. 인터넷 연결성 체크 우회하기 (더미 HTTP 서버)

SansaApp은 MQTT 연결을 시도하기 전에 www.baidu.com으로 인터넷 연결 여부를 먼저 확인하는 로직이 내장되어 있습니다 (Qingping 계열 공통 특징으로 보입니다). TTL=2 설정 때문에 이 요청이 항상 타임아웃되면, 기기가 “네트워크 연결 없음” 판정을 내리고 MQTT 연결 단계까지 진행하지 못하는 문제가 발생합니다.

해결책: www.baidu.com도 로컬 IP로 리다이렉트하고, 그 IP에 HTTP 요청에 응답만 해주는 더미 웹서버를 띄웁니다.

# 기기 쪽 (adb shell)
echo "192.168.219.137 www.baidu.com" >> /etc/hosts

로컬 서버 쪽에서 간단한 더미 HTTP 서버 실행 (포트 80):

sudo python3 -m http.server 80

기기에서 테스트:

curl -I http://www.baidu.com

HTTP/1.0 200 OK 계열 응답이 나오면 성공입니다.

참고: MQTT 브로커와 더미 HTTP 서버는 같은 머신에 둘 필요 없습니다. 각각 다른 IP에 떠 있어도 되며, hosts 파일에서 각 도메인마다 원하는 IP를 지정하면 됩니다.


11. 재부팅 후 연결 확인

모든 설정을 마쳤으면 재부팅합니다.

reboot

기기가 다시 부팅되면, 로컬 서버에서 MQTT 구독을 열어두고 데이터가 들어오는지 확인합니다.

mosquitto_sub -h 192.168.219.137 -t '#' -v

동시에 기기 쪽 로그도 실시간으로 확인하면 문제 진단에 도움이 됩니다.

adb shell
tail -f /data/etc/log/$(date '+%Y-%m-%d').log | grep -i mqtt

정상적으로 연결되면 아래와 같은 로그가 반복적으로(주기적 재연결 포함) 찍힙니다.

[DEBUG][MQTT_APP-...](待上报数据: { ... )
[DEBUG][MQTT_APP-...](hostname:mqtt.ko.cleargrass.com, host:127.0.0.1, port:1883, ...)
[DEBUG][MQTT_APP-...](APP 已连接云端)
[DEBUG][MQTT_APP-...](mqtt上线后,订阅sansa/command/{device_id})

몇 분(예시 기기 기준 8~20분 주기) 내에 mosquitto_sub 쪽에서도 데이터 메시지가 수신되기 시작합니다.


12. 수신되는 MQTT 페이로드 구조

토픽

sansa/data/{device_id}

최상위 JSON 구조

{
  "device_id": "51D0D9713C3F11EB813200163E000886",
  "sensor_data": "<base64 인코딩된 문자열>",
  "signature": "...",
  "timestamp": 1785048053,
  "version": "1.1.38"
}

sensor_data 필드는 base64로 인코딩된 JSON입니다. 디코딩하면 실제 센서 값이 나옵니다.

echo "<sensor_data 값>" | base64 -d

디코딩된 센서 JSON 예시

{
  "temperature": {"status":"sampling","timestamp":1785048053,"unit":"C","value":2877},
  "humidity":    {"status":"sampling","timestamp":1785048053,"unit":"%","value":6540},
  "illuminance": {"timestamp":1785048053,"unit":"lux","value":11},
  "co2":         {"status":"sampling","level":"moderate","value":127200},
  "pm1":         {"level":"good","value":500},
  "pm2":         {"level":"good","value":520},
  "pm10":        {"level":"good","value":1020},
  "tvoc":        {"level":"good","value":9300},
  "battery":     {"status":"discharging","value":100}
}

value는 대부분 실제값의 100배로 스케일업되어 옵니다. 예: temperature.value = 2877 → 실제 28.77°C co2.value = 127200 → 실제 1272 ppm (조도(illuminance)와 배터리(battery)는 예외로 스케일 없이 그대로 옵니다.)

각 항목의 level 필드는 다음과 같은 상태값으로 옵니다: excellent, good, moderate, unhealthy, very_unhealthy, hazardous


13. Home Assistant 연동 (mqtt.yaml)

configuration.yaml에서 MQTT 통합의 센서 설정 파일로 아래 내용을 불러오도록 구성한 뒤(mqtt: !include mqtt.yaml 등), 다음과 같이 작성합니다. (아래는 실제 성공 사례에서 사용한 설정입니다. {device_id} 부분과 이름(서재)은 각자 환경에 맞게 바꾸세요.)

sensor:
  - name: "서재 에어모니터 온도"
    unique_id: air_monitor_temperature
    state_topic: "sansa/data/51D0D9713C3F11EB813200163E000886"
    device_class: temperature
    unit_of_measurement: "°C"
    state_class: measurement
    suggested_display_precision: 2
    value_template: >-
      {% set data = value_json.sensor_data | base64_decode | from_json %}
      {% if data.temperature is defined %}
        {{ (data.temperature.value | float / 100) | round(2) }}
      {% else %}
        {{ '' }}
      {% endif %}
    device: &air_monitor_device
      identifiers:
        - samsung_air_monitor_acm_b1mos
      name: "서재 에어모니터"
      manufacturer: "Samsung"
      model: "ACM-B1MOS"
      sw_version: "1.1.38"

  - name: "서재 에어모니터 습도"
    unique_id: air_monitor_humidity
    state_topic: "sansa/data/51D0D9713C3F11EB813200163E000886"
    device_class: humidity
    unit_of_measurement: "%"
    state_class: measurement
    suggested_display_precision: 2
    value_template: >-
      {% set data = value_json.sensor_data | base64_decode | from_json %}
      {% if data.humidity is defined %}
        {{ (data.humidity.value | float / 100) | round(2) }}
      {% else %}
        {{ '' }}
      {% endif %}
    device: *air_monitor_device

  - name: "서재 에어모니터 이산화탄소"
    unique_id: air_monitor_co2
    state_topic: "sansa/data/51D0D9713C3F11EB813200163E000886"
    device_class: carbon_dioxide
    unit_of_measurement: "ppm"
    state_class: measurement
    suggested_display_precision: 0
    value_template: >-
      {% set data = value_json.sensor_data | base64_decode | from_json %}
      {% if data.co2 is defined %}
        {{ (data.co2.value | float / 100) | round(0) }}
      {% else %}
        {{ '' }}
      {% endif %}
    device: *air_monitor_device

  - name: "서재 에어모니터 극초미세먼지 PM1"
    unique_id: air_monitor_pm1
    state_topic: "sansa/data/51D0D9713C3F11EB813200163E000886"
    device_class: pm1
    unit_of_measurement: "µg/m³"
    state_class: measurement
    suggested_display_precision: 1
    value_template: >-
      {% set data = value_json.sensor_data | base64_decode | from_json %}
      {% if data.pm1 is defined %}
        {{ (data.pm1.value | float / 100) | round(1) }}
      {% else %}
        {{ '' }}
      {% endif %}
    device: *air_monitor_device

  - name: "서재 에어모니터 초미세먼지 PM2.5"
    unique_id: air_monitor_pm25
    state_topic: "sansa/data/51D0D9713C3F11EB813200163E000886"
    device_class: pm25
    unit_of_measurement: "µg/m³"
    state_class: measurement
    suggested_display_precision: 1
    value_template: >-
      {% set data = value_json.sensor_data | base64_decode | from_json %}
      {% if data.pm2 is defined %}
        {{ (data.pm2.value | float / 100) | round(1) }}
      {% else %}
        {{ '' }}
      {% endif %}
    device: *air_monitor_device

  - name: "서재 에어모니터 미세먼지 PM10"
    unique_id: air_monitor_pm10
    state_topic: "sansa/data/51D0D9713C3F11EB813200163E000886"
    device_class: pm10
    unit_of_measurement: "µg/m³"
    state_class: measurement
    suggested_display_precision: 1
    value_template: >-
      {% set data = value_json.sensor_data | base64_decode | from_json %}
      {% if data.pm10 is defined %}
        {{ (data.pm10.value | float / 100) | round(1) }}
      {% else %}
        {{ '' }}
      {% endif %}
    device: *air_monitor_device

  - name: "서재 에어모니터 TVOC"
    unique_id: air_monitor_tvoc
    state_topic: "sansa/data/51D0D9713C3F11EB813200163E000886"
    device_class: volatile_organic_compounds_parts
    unit_of_measurement: "ppb"
    state_class: measurement
    suggested_display_precision: 0
    value_template: >-
      {% set data = value_json.sensor_data | base64_decode | from_json %}
      {% if data.tvoc is defined %}
        {{ (data.tvoc.value | float / 100) | round(0) }}
      {% else %}
        {{ '' }}
      {% endif %}
    device: *air_monitor_device

  - name: "서재 에어모니터 조도"
    unique_id: air_monitor_illuminance
    state_topic: "sansa/data/51D0D9713C3F11EB813200163E000886"
    device_class: illuminance
    unit_of_measurement: "lx"
    state_class: measurement
    suggested_display_precision: 0
    value_template: >-
      {% set data = value_json.sensor_data | base64_decode | from_json %}
      {% if data.illuminance is defined %}
        {{ data.illuminance.value | float | round(0) }}
      {% else %}
        {{ '' }}
      {% endif %}
    device: *air_monitor_device

  - name: "서재 에어모니터 배터리"
    unique_id: air_monitor_battery
    state_topic: "sansa/data/51D0D9713C3F11EB813200163E000886"
    device_class: battery
    unit_of_measurement: "%"
    state_class: measurement
    suggested_display_precision: 0
    value_template: >-
      {% set data = value_json.sensor_data | base64_decode | from_json %}
      {% if data.battery is defined %}
        {{ data.battery.value | int }}
      {% else %}
        {{ '' }}
      {% endif %}
    device: *air_monitor_device

level(상태 등급) 값이나 PM1/PM2.5/PM10/TVOC 상태 센서, 배터리 충방전 상태 등 추가 엔티티도 동일한 패턴(value_json.sensor_data | base64_decode | from_json)으로 얼마든지 확장할 수 있습니다.

state_topic의 device_id 부분을 각자 5번 단계에서 알아낸 본인 기기의 ID로 반드시 바꿔주세요.


14. 고급: 명령 토픽으로 폴링 주기 단축하기

기본적으로 기기는 8~20분 정도의 자체 주기로만 sansa/data/{device_id} 토픽에 센서값을 publish합니다. 더 빠른 주기로 값을 받고 싶다면, 기기가 구독하고 있는 sansa/command/{device_id} 토픽으로 명령을 보내 강제로 리포팅을 트리거할 수 있는 것으로 보입니다.

원리

SansaAppsansa/command/{device_id} 토픽을 구독해서 원격 명령을 받는데, “투오”님의 실측을 통해 아래 내용이 확인되었습니다.

  • cid:200이 실제로 리포팅(데이터 즉시 전송)을 트리거하는 명령입니다.
  • 먼저 보낸 cid:201은 특별한 의미가 있는 값이 아니라, 임의의 값을 넣어도
    무방
    한 것으로 확인되었습니다 (자리 채우기용에 가깝습니다).
  • 단, cid:200을 연속으로 두 번 보내면 동작하지 않습니다.200 단독
    반복 호출로는 리포팅이 트리거되지 않고, 앞에 다른 값(예: 201)을 한 번 보낸
    200을 보내는 “두 단계” 패턴이어야 정상 동작하는 것으로 보입니다. (아마도
    기기 내부에 상태 변화(state change)를 감지해서 반응하는 로직이 있고,
    200→200처럼 값이 같으면 변화가 없다고 판단해 무시하는 구조로 추정됩니다.)
cid확인된 역할
201 (예시)특별한 의미 없음, 임의값으로 대체 가능 — 직전 값과 다르기만 하면 됨
200리포팅(데이터 즉시 전송) 트리거

정확한 내부 로직(왜 연속된 200이 무시되는지)은 공식적으로 확인된 바 없고, 관찰된 동작을 기반으로 한 추정입니다. 혹시 다음 주기에도 이 패턴이 계속 안정적으로 동작하는지는 지속적으로 모니터링하는 것을 권장합니다.

Home Assistant 자동화 예시

sansa/command/{device_id}{device_id}는 각자 기기의 ID로 바꿔서 사용하세요 (아래 예시는 임의의 다른 기기 ID 7EF6FAA4016711EBA78F00163E000886 기준입니다).

alias: 에어모니터 1분 폴링
description: "1분마다 에어모니터의 현재 센서값 전체 리포팅 요청"
mode: single
triggers:
  - trigger: time_pattern
    minutes: "/1"
conditions: []
actions:
  - action: mqtt.publish
    data:
      topic: "sansa/command/7EF6FAA4016711EBA78F00163E000886"
      payload: '{"cid":201}'
      qos: 0
      retain: false
  - delay:
      milliseconds: 250
  - action: mqtt.publish
    data:
      topic: "sansa/command/7EF6FAA4016711EBA78F00163E000886"
      payload: '{"cid":200}'
      qos: 0
      retain: false

위 예시에서 첫 번째 cid:201은 특정 값이라서 의미가 있는 게 아니라, 두 번째 cid:200과 다른 값이기만 하면 됩니다. 즉 201 대신 다른 임의의 숫자를 넣어도 무방합니다. 다만 두 스텝 모두 200으로 넣으면 동작하지 않으니 주의하세요.

ℹ️ time_pattern 트리거의 seconds 값은 0~59 범위만 허용됩니다 (seconds: "/60"처럼 60 이상은 저장 시 검증 오류가 납니다). 1분마다 실행하려면 위 예시처럼 **minutes: "/1"**을 사용하세요 — 매 분 0초에 한 번씩 실행되어 결과적으로 1분 간격과 동일하게 동작합니다.

검증 방법

자동화를 켠 뒤, 아래 명령으로 실제로 1분 주기로 데이터가 올라오는지 직접 확인하세요.

mosquitto_sub -h <브로커IP> -u <username> -P <password> -t 'sansa/data/#' -v

원래의 자연 주기(8~20분)보다 훨씬 자주 sansa/data/{device_id} 메시지가 찍히면 성공입니다. 너무 짧은 주기(예: 몇 초 단위)로는 시도하지 않는 것을 권장합니다 — 기기 리소스나 배터리 소모, 혹은 예상치 못한 부작용이 있을 수 있습니다.


15. HAOS Mosquitto 애드온에서 기존 브로커로 계정 추가하기

지금까지는 임시로 별도 서버(예: 192.168.219.137)에 allow_anonymous true로 띄운 브로커를 썼습니다. 하지만 실제 운영 환경에서는 이미 사용 중인 HAOS의 Mosquitto broker 애드온(예: 192.168.219.130)으로 옮기는 것이 정리된 구성입니다. 이 브로커는 보통 계정별 인증(username/password)을 쓰기 때문에, 에어모니터가 보내는 인증 정보를 브로커에 등록해줘야 합니다.

14-1. 기기가 사용 중인 인증 정보 확인

앞서 5~6번 단계에서 살펴본 MQTT 연결 로그에 인증 정보가 그대로 찍혀 있습니다.

cat /data/etc/log/$(date '+%Y-%m-%d').log | grep -i "MQTT_APP" | grep "username"

예시:

[DEBUG][MQTT_APP-...](hostname:mqtt.ko.cleargrass.com,host:127.0.0.1,port:1883,
client ID:51D0D9713C3F11EB813200163E000886,
username:12219e07ffa94ce1a98349802f59e2d5,
password:96cf69c7e05444f3bf8696c950703205)

여기서 usernamepassword 값을 그대로 사용합니다. 이 값은 기기가 이미 정해서 보내는 고정값이라 우리가 임의로 바꿀 수 없고, 브로커 쪽에 이 값 그대로 계정을 만들어줘야 합니다.

14-2. 애드온 설정에 계정 추가

시스템 셸에 직접 접속할 필요 없이, HA 프론트엔드에서 처리합니다.

  1. 설정 → 애드온(Add-ons) → Mosquitto broker 이동
  2. Configuration(구성) 탭 → YAML 편집 모드
  3. logins 항목에 아래처럼 추가 (기존에 다른 logins 항목이 있다면 그 아래에
    이어서 추가):
logins:
  - username: 12219e07ffa94ce1a98349802f59e2d5
    password: 96cf69c7e05444f3bf8696c950703205
    password_pre_hashed: false
customize:
  active: false
  folder: mosquitto
  1. 저장 → 애드온 재시작

password_pre_hashed 옵션이란?

애드온이 password 값을 평문으로 받아 자체적으로 해시할지, 아니면 이미 해시된 값으로 간주하고 그대로 저장할지를 결정하는 옵션입니다.

동작
false (기본값)입력한 값을 평문 비밀번호로 취급해 애드온이 내부적으로 해시 처리 후 저장
true입력한 값을 이미 해시된 문자열로 간주하고 추가 해싱 없이 그대로 저장

에어모니터 로그에서 확인한 password 값은 기기가 MQTT 접속 시 그대로 전송하는 평문 비밀번호이므로, **password_pre_hashed: false**로 두면 됩니다 (기본값이라 이 줄 자체를 생략해도 동일하게 동작).

참고: 대안으로 설정 → 사람(People) → 사용자(Users) 에서 새 사용자를 만들어도 MQTT 인증이 되지만, 이 방식은 HA 웹 로그인 계정도 같이 생성되는 부작용이 있어 에어모니터 같은 순수 디바이스 인증에는 위 logins 방식을 권장합니다.

14-3. 기기 hosts를 기존 브로커 IP로 변경

adb shell
sed -i '/mqtt.ko.cleargrass.com/d' /etc/hosts
echo "192.168.219.130 mqtt.ko.cleargrass.com" >> /etc/hosts
cat /etc/hosts
reboot

www.baidu.com 더미 응답용 HTTP 서버(10번 단계)는 어느 IP에 떠 있어도 무방하지만, 계속 응답 가능한 상태로 유지되어야 합니다.

14-4. 인증 포함 연결 확인

mosquitto_sub -h 192.168.219.130 \
  -u 12219e07ffa94ce1a98349802f59e2d5 \
  -P 96cf69c7e05444f3bf8696c950703205 \
  -t 'sansa/#' -v

데이터가 정상적으로 수신되면, 임시로 썼던 .137 브로커는 정리해도 됩니다.


16. 트러블슈팅

HAOS 브로커에서 disconnected: not authorised 에러가 뜬다

  • 애드온 Configuration 탭에서 logins 항목을 추가한 뒤 저장(Save) 버튼을 누르지
    않은 경우
    가장 흔하게 발생합니다. 항목을 “추가”만 하고 화면을 벗어나면 반영되지
    않으니, 반드시 저장 → 애드온 재시작 순서를 지키세요.
  • 재시작 후 애드온 로그 탭에서 Setting up user <username> 같은 줄이 찍히는지
    확인하면 실제로 반영됐는지 바로 알 수 있습니다.
  • 그래도 안 되면 username/password에 복사 과정에서 공백이나 줄바꿈이 섞여 들어가지
    않았는지 확인하세요.

mosquitto_sub에 아무것도 안 뜬다

  • /etc/hosts에 MQTT 도메인이 정확히 (오타 없이) 들어갔는지 확인
  • 로컬 서버 방화벽에서 1883 포트가 막혀있지 않은지 확인
  • 기기 로그(/data/etc/log/...log)에서 MQTT_APP 관련 줄을 grep해서 실제로
    연결을 “시도”라도 하는지 확인
  • 기존에 다른 MQTT 클라이언트(예: Zigbee2MQTT 등)가 같은 브로커에 붙어 있으면
    # 와일드카드 구독 시 그쪽 메시지에 묻힐 수 있으니, sansa/#처럼 좁혀서 구독

화면에 “인터넷 연결 안 됨” 경고(!)가 뜬다

  • TTL=2 설정 때문에 정상적으로 발생하는 현상입니다. 로컬 네트워크(1홉) 안의
    MQTT/HTTP 통신은 문제없이 되고, 실제 원격 인터넷만 막힌 상태입니다.
  • 다만 www.baidu.com 연결성 체크가 실패하면 앱이 MQTT 단계까지 못 가는 경우가
    있으니 10번 단계(더미 HTTP 서버)를 꼭 확인하세요.

로그에 Host ... not found 에러가 계속 쌓인다

  • sansa.cleargrass.com(날씨/시간 동기화용 HTTPS API)은 hosts에 등록 안 해도
    MQTT 센서 데이터 수신 자체에는 영향 없습니다. 로그가 지저분한 게 싫다면 이
    도메인도 더미 서버로 리다이렉트하면 에러가 사라집니다.

설정이 재부팅/초기화 후 사라진다

  • /etc/, /data/etc/init.d/ 하위는 파티션에 따라 오버레이(overlay) 파일시스템일
    수 있어 완전 초기화 시 날아갈 수 있습니다. 이 문서의 설정 스크립트를 파일로
    저장해두고, 필요시 다시 실행하는 방식을 권장합니다.

17. 전체 명령어 요약

기기 쪽(adb shell)에서 한 번에 실행할 수 있는 요약본입니다. 도메인명, IP, device_id는 반드시 본인 기기에서 확인한 값으로 교체하세요.

# 1. 기기 ID / 도메인 확인 (먼저 실행해서 본인 값 확인)
cat /data/etc/log/$(date '+%Y-%m-%d').log | grep -i "device_id\|ClientID\|hostname"

# 2. 프로세스 정리
killall watchdog.sh
killall SansaApp

# 3. 네트워크 격리 (TTL 트릭)
cat << EOF > /etc/sysctl.conf
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
net.ipv6.conf.lo.disable_ipv6 = 1
net.ipv4.ip_default_ttl=2
EOF

cat << EOF > /etc/init.d/S52custom
/sbin/sysctl -p /etc/sysctl.conf
EOF
chmod +x /etc/init.d/S52custom

# 4. hosts 리다이렉트 (본인 환경에 맞게 도메인/IP 수정)
cat << EOF >> /etc/hosts
192.168.219.137 mqtt.ko.cleargrass.com
192.168.219.137 www.baidu.com
EOF

# 5. 재부팅
reboot

로컬 서버 쪽에서 (별도 터미널):

# MQTT 브로커
sudo apt install -y mosquitto mosquitto-clients
sudo tee /etc/mosquitto/conf.d/local.conf << 'EOF'
listener 1883 0.0.0.0
allow_anonymous true
EOF
sudo systemctl restart mosquitto

# 더미 HTTP 서버 (baidu.com 연결성 체크 통과용)
sudo python3 -m http.server 80

# 확인
mosquitto_sub -h <로컬서버IP> -t 'sansa/#' -v

참고

  • 이 기기는 Qingping OEM 하드웨어를 기반으로 하며, 커뮤니티에는 Qingping 순정
    기기를 위한 공식 “Private MQTT” 기능(개발자 콘솔 등록 방식)도 존재합니다.
    다만 삼성 리브랜딩 모델은 SmartThings 전용 페어링 구조라 그 방식이 통하지 않아,
    이 문서에서는 ADB/로컬 네트워크 리다이렉트 방식을 사용했습니다.
  • 이 문서에 포함된 device_id(51D0D9713C3F11EB813200163E000886), IP 대역
    등은 예시 기기 기준 값이며, 각자의 기기/네트워크 환경에 맞게 반드시
    본인 값으로 교체해서 사용하세요.