ACM-B1MOS-local-Home-Assistant

삼성 에어모니터 ACM-B1MOS 로컬 Home Assistant 연동

1. 목적과 현재 상태

삼성 에어모니터 스탠다드 ACM-B1MOS의 센서 값을 SmartThings 클라우드를 거치지 않고 로컬 MQTT와 Home Assistant에서 사용하기 위한 작업 기록이다.

현재 다음 항목까지 확인했다.

  • 장치 IP: 192.168.1.51


  • 장치 모델: Samsung ACM-B1MOS


  • MQTT 서버 도메인: mqtt.ko.cleargrass.com


  • HTTPS 서버 도메인: sansa.cleargrass.com


  • MQTT 프로토콜: MQTT 3.1.1, 평문 TCP 1883


  • MQTT command topic:


    sansa/command/7EF6FAA4016711EBA78F00163E000886

  • MQTT data topic:


    sansa/data/7EF6FAA4016711EBA78F00163E000886

  • MQTT 명령으로 로컬 센서 데이터 발행 확인 (up_itvl: 10의 정확한 10초 고정 주기 동작은 미확인)


  • Base64로 감싼 JSON 센서 데이터 디코딩 확인


  • Home Assistant MQTT 센서 구성 확인


이 문서에서는 Mosquitto 설치나 일반적인 브로커 설정 방법은 다루지 않는다. 이미 사용할 수 있는 MQTT 브로커가 있다는 전제에서 DNS 우회, 인증 선택, 패킷 캡처, Home Assistant 등록만 설명한다.

2. 확인된 네트워크 동작

장치를 완전히 부팅하면 다음 DNS 조회와 연결이 발생한다.

mqtt.ko.cleargrass.com  → MQTT TCP 1883
sansa.cleargrass.com    → HTTPS TCP 443
www.baidu.com           → 인터넷 연결 확인으로 추정

캡처 당시 외부 주소는 다음과 같았다. 외부 IP는 바뀔 수 있으므로 고정값으로 사용하면 안 된다.

mqtt.ko.cleargrass.com → 39.106.170.175
sansa.cleargrass.com   → 161.117.191.11

장치는 공유기 192.168.1.1을 DNS 서버로 사용했다. 따라서 공유기 DNS에서 MQTT 도메인만 내부 브로커로 돌리면 된다.

mqtt.ko.cleargrass.com → 내부 MQTT 브로커 IP

sansa.cleargrass.com은 HTTPS이므로 처음에는 우회하지 않는다. 이 도메인까지 내부 서버로 변경하면 장치의 TLS 인증서 검증 때문에 통신이 실패할 수 있다.

3. MQTT 연결 방식 선택

옵션 1: 익명 MQTT 브로커 사용

가장 빠르고 단순한 방법이다.

ACM-B1MOS
  → mqtt.ko.cleargrass.com DNS 우회
  → 익명 접속 허용 MQTT 브로커:1883
  → Home Assistant

브로커가 익명 접속을 허용하면 장치의 원래 MQTT ID/PW를 알아낼 필요가 없다. 장치는 CONNECT 패킷에 자체 사용자명을 보내더라도 익명 접속을 허용하는 브로커에 정상 연결할 수 있다.

이 방식은 다음 경우에 적합하다.

  • MQTT 브로커가 IoT 전용 내부망에 있음
  • 외부에서 TCP 1883에 접근할 수 없음
  • 장치 인증정보를 추출하지 않고 빠르게 연동하려는 경우
  • 분석용 또는 전용 브로커를 별도 IP에서 운영하는 경우

익명 브로커는 인터넷에 노출하지 않는다. 최소한 공유기 방화벽에서 LAN 내부 접속만 허용해야 한다.

옵션 2: 인증 MQTT 브로커 사용

기존 Home Assistant Mosquitto처럼 사용자명과 비밀번호가 필수인 브로커를 직접 사용하는 방식이다.

ACM-B1MOS
  → mqtt.ko.cleargrass.com DNS 우회
  → 기존 HA MQTT 브로커:1883
  → Home Assistant

이 경우 장치가 보내는 원래 MQTT 사용자명과 비밀번호를 캡처하고, 동일한 인증정보를 대상 브로커에 등록해야 한다.

장점은 다음과 같다.

  • 기존 Home Assistant MQTT 브로커를 그대로 사용
  • 별도 분석 브로커와 MQTT bridge가 불필요
  • 동일 IP의 TCP 1883 포트 충돌이 없음
  • 인증되지 않은 LAN 클라이언트의 접속을 차단 가능

장치 인증정보를 브로커에 등록하는 구체적인 방법은 브로커 배포 방식마다 다르므로 이 문서에서는 다루지 않는다.

4. Synology RT2600ac에서 DNS 우회 확인

RT2600ac SRM 1.3의 기본 LAN 인터페이스는 일반적으로 lbr0이다. 실제 인터페이스를 먼저 확인한다.

ip route | grep 192.168.1

공유기 DNS에 다음 로컬 레코드를 등록한다.

mqtt.ko.cleargrass.com → MQTT_BROKER_IP

예를 들어 내부 브로커가 192.168.1.13이면 다음 결과가 나와야 한다.

nslookup mqtt.ko.cleargrass.com 192.168.1.1
Name:    mqtt.ko.cleargrass.com
Address: 192.168.1.13

DNS와 MQTT 접속을 동시에 확인하려면 공유기에서 다음을 실행한다.

tcpdump -ni lbr0 -vv \
  'host 192.168.1.51 and (port 53 or tcp port 1883)'

정상 동작 예:

192.168.1.51 → 192.168.1.1:53
192.168.1.1  → 192.168.1.51: mqtt.ko.cleargrass.com A 192.168.1.13
192.168.1.51 → 192.168.1.13:1883 SYN

tcpdump의 다음 메시지는 공유기의 체크섬 오프로딩 때문에 나타날 수 있으며 DNS 오류를 의미하지 않는다.

bad udp cksum

5. 장치를 제대로 재부팅하는 방법

ACM-B1MOS에는 내장 배터리가 있다. USB-C 전원만 분리해도 장치가 계속 실행되므로 DNS 캐시와 MQTT 세션이 유지될 수 있다.

DNS 변경 후에는 다음 절차로 완전히 재부팅한다.

  1. 장치 메뉴 또는 전원 버튼으로 화면이 완전히 꺼질 때까지 종료한다.
  2. 10초 정도 기다린다.
  3. 장치를 다시 켠다.
  4. 공유기의 tcpdump와 MQTT 로그에서 재접속을 확인한다.

MQTT 로그에는 장치 IP가 명확히 나타나야 한다.

New connection from 192.168.1.51
New client connected ... as 7EF6FAA4016711EBA78F00163E000886

127.0.0.1에서 접속한 auto-... 클라이언트는 로컬에서 실행한 mosquitto_sub이며 ACM-B1MOS가 아니다.

6. 옵션 2용 MQTT ID/PW 추출

옵션 1의 익명 MQTT를 사용할 때는 이 과정이 필요 없다.

MQTT TCP 1883은 암호화되지 않았으므로 CONNECT 패킷 안의 Client ID, 사용자명, 비밀번호를 PCAP에서 확인할 수 있다.

6.1 공유기에서 CONNECT 패킷 캡처

tcpdump -ni lbr0 -s 0 \
  'host 192.168.1.51 and tcp port 1883' \
  -w /tmp/acm-mqtt-auth.pcap

캡처를 실행한 상태에서 장치를 완전히 재부팅한다. MQTT 접속이 끝난 뒤 Ctrl+C로 종료한다.

구형 SRM에서 PC로 가져올 때 최신 SCP의 SFTP 모드가 동작하지 않으면 기존 SCP 프로토콜을 강제한다.

scp -O root@192.168.1.1:/tmp/acm-mqtt-auth.pcap .

SSH RSA 호환 오류가 발생하면 필요한 옵션만 추가한다.

scp -O `
  -o HostKeyAlgorithms=+ssh-rsa `
  -o PubkeyAcceptedAlgorithms=+ssh-rsa `
  root@192.168.1.1:/tmp/acm-mqtt-auth.pcap .

6.2 Wireshark에서 확인

표시 필터:

mqtt.msgtype == 1

MQTT CONNECT 패킷에서 다음 필드를 확인한다.

Client ID
User Name
Password

6.3 Windows tshark로 추출

& "C:\Program Files\Wireshark\tshark.exe" `
  -r ".\acm-mqtt-auth.pcap" `
  -Y "mqtt.msgtype == 1" `
  -T fields `
  -E "header=y" `
  -E "quote=d" `
  -e mqtt.clientid `
  -e mqtt.username `
  -e mqtt.passwd

출력된 사용자명과 비밀번호를 인증 MQTT 브로커에 동일하게 등록한다. 비밀번호와 인증 PCAP은 공개 저장소나 채팅에 올리지 않는다.

7. 로컬 MQTT 통신 활성화

장치는 연결 직후 다음 command topic을 구독한다.

sansa/command/7EF6FAA4016711EBA78F00163E000886

서버가 아무 명령도 보내지 않으면 장치는 약 1초 후 연결을 종료할 수 있다. 다음 메시지를 retained로 발행하면 센서 데이터 보고를 요청할 수 있다.

{"type":"12","up_itvl":"10","duration":"86400"}

동작 여부 주의: 이 명령으로 데이터 발행이 시작되는 것은 확인했지만, ACM-B1MOS가 up_itvl: 10을 정확한 10초 고정 주기로 지키는지는 확인되지 않았다. 실제 관찰에서는 메시지가 약 20초~1분 간격으로 불규칙하게 도착했다. 온도·습도 묶음과 공기질 묶음의 측정 및 발행 시점도 서로 다르므로 10은 현재 10초 보고 요청값으로 취급한다.

type: 12up_itvl: 10Qingping 공식 MQTT 문서의 임시 보고 예제에서 가져왔다. 공식 예제의 duration60초다. duration: 86400은 패킷 캡처에서 추출한 값이 아니라, Home Assistant 사용자 설정 사례처럼 명령을 24시간 유지하기 위해 확장한 값이다. 따라서 위 전체 명령은 ACM-B1MOS의 원래 서버 명령을 그대로 복원한 것이 아니다.

의미:

type       12       임시 센서 보고 설정
up_itvl    10       10초 간격 보고 요청(정확한 주기 동작 여부 미확인)
duration   86400    24시간 유지

이 명령은 Home Assistant가 10초마다 장치를 폴링한다는 뜻이 아니다. Home Assistant가 명령을 한 번 보내 장치에 임시 보고 모드를 요청한다. 장치는 지정된 기간 동안 MQTT로 값을 push하지만, ACM-B1MOS에서 실제 발행 간격은 정확히 10초로 고정되지 않고 달라질 수 있다.

장치는 다음 data topic으로 값을 발행한다.

sansa/data/7EF6FAA4016711EBA78F00163E000886

정상 브로커 로그:

Received SUBSCRIBE from 7EF6...
Sending PUBLISH to 7EF6... 'sansa/command/7EF6...'
Received PUBLISH from 7EF6... 'sansa/data/7EF6...'

8. 확인된 MQTT payload 구조

외부 JSON의 sensor_data가 Base64 문자열이며, 디코딩 결과도 JSON이다.

센서 데이터는 한 메시지에 전부 들어오지 않고 크게 두 묶음으로 나뉘어 전송됐다.

환경 센서 묶음

{
  "humidity": {
    "status": "sampling",
    "timestamp": 1785033480,
    "unit": "%",
    "value": 5639
  },
  "illuminance": {
    "timestamp": 1785033480,
    "unit": "lux",
    "value": 1
  },
  "temperature": {
    "status": "sampling",
    "timestamp": 1785033480,
    "unit": "C",
    "value": 2554
  }
}

공기질 센서 묶음

{
  "battery": {
    "status": "discharge",
    "value": 90
  },
  "co2": {
    "level": "unhealthy",
    "status": "sampling",
    "unit": "ppm",
    "value": 190700
  },
  "illuminance": {
    "unit": "lux",
    "value": 1
  },
  "pm1": {
    "level": "good",
    "status": "sampling",
    "unit": "µg/m³",
    "value": 500
  },
  "pm2": {
    "level": "good",
    "status": "sampling",
    "unit": "µg/m³",
    "value": 500
  },
  "pm10": {
    "level": "good",
    "status": "sampling",
    "unit": "µg/m³",
    "value": 500
  },
  "tvoc": {
    "level": "good",
    "status": "sampling",
    "unit": "ppb",
    "value": 9100
  }
}

확인된 스케일:

필드원시 값 예HA 값변환
temperature255425.54 °C/ 100
humidity563956.39 %/ 100
co21907001907 ppm/ 100
pm15005 µg/m³/ 100
pm25005 µg/m³/ 100
pm105005 µg/m³/ 100
tvoc910091 ppb/ 100
illuminance11 lx그대로
battery9090 %그대로

pm2는 ACM-B1MOS의 PM2.5 값이다. TVOC 9100이면서 level: good이므로 실제 값은 91 ppb로 해석한다.

9. Home Assistant 파일 구성

configuration.yaml:

mqtt: !include mqtt.yaml

mqtt.yaml에는 mqtt:를 다시 쓰지 않고 바로 sensor:부터 작성한다.

동일한 device.identifiers를 사용하면 여러 센서 엔티티가 Home Assistant의 에어모니터 장치 하나 아래에 묶인다.

10. mqtt.yaml

sensor:
  - name: "온도"
    unique_id: air_monitor_temperature
    state_topic: "sansa/data/7EF6FAA4016711EBA78F00163E000886"
    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/7EF6FAA4016711EBA78F00163E000886"
    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/7EF6FAA4016711EBA78F00163E000886"
    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: "이산화탄소 상태"
    unique_id: air_monitor_co2_level
    state_topic: "sansa/data/7EF6FAA4016711EBA78F00163E000886"
    icon: mdi:molecule-co2
    value_template: >-
      {% set data = value_json.sensor_data | base64_decode | from_json %}
      {% set level = data.co2.level %}
      {% set labels = {
        'excellent': '매우 좋음',
        'good': '좋음',
        'moderate': '보통',
        'unhealthy': '나쁨',
        'very_unhealthy': '매우 나쁨',
        'hazardous': '위험'
      } %}
      {{ labels.get(level, level) }}
    device: *air_monitor_device

  - name: "극초미세먼지 PM1"
    unique_id: air_monitor_pm1
    state_topic: "sansa/data/7EF6FAA4016711EBA78F00163E000886"
    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: "PM1 상태"
    unique_id: air_monitor_pm1_level
    state_topic: "sansa/data/7EF6FAA4016711EBA78F00163E000886"
    icon: mdi:air-filter
    value_template: >-
      {% set data = value_json.sensor_data | base64_decode | from_json %}
      {% set level = data.pm1.level %}
      {% set labels = {
        'excellent': '매우 좋음',
        'good': '좋음',
        'moderate': '보통',
        'unhealthy': '나쁨',
        'very_unhealthy': '매우 나쁨',
        'hazardous': '위험'
      } %}
      {{ labels.get(level, level) }}
    device: *air_monitor_device

  - name: "초미세먼지 PM2.5"
    unique_id: air_monitor_pm25
    state_topic: "sansa/data/7EF6FAA4016711EBA78F00163E000886"
    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: "PM2.5 상태"
    unique_id: air_monitor_pm25_level
    state_topic: "sansa/data/7EF6FAA4016711EBA78F00163E000886"
    icon: mdi:air-filter
    value_template: >-
      {% set data = value_json.sensor_data | base64_decode | from_json %}
      {% set level = data.pm2.level %}
      {% set labels = {
        'excellent': '매우 좋음',
        'good': '좋음',
        'moderate': '보통',
        'unhealthy': '나쁨',
        'very_unhealthy': '매우 나쁨',
        'hazardous': '위험'
      } %}
      {{ labels.get(level, level) }}
    device: *air_monitor_device

  - name: "미세먼지 PM10"
    unique_id: air_monitor_pm10
    state_topic: "sansa/data/7EF6FAA4016711EBA78F00163E000886"
    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: "PM10 상태"
    unique_id: air_monitor_pm10_level
    state_topic: "sansa/data/7EF6FAA4016711EBA78F00163E000886"
    icon: mdi:air-filter
    value_template: >-
      {% set data = value_json.sensor_data | base64_decode | from_json %}
      {% set level = data.pm10.level %}
      {% set labels = {
        'excellent': '매우 좋음',
        'good': '좋음',
        'moderate': '보통',
        'unhealthy': '나쁨',
        'very_unhealthy': '매우 나쁨',
        'hazardous': '위험'
      } %}
      {{ labels.get(level, level) }}
    device: *air_monitor_device

  - name: "TVOC"
    unique_id: air_monitor_tvoc
    state_topic: "sansa/data/7EF6FAA4016711EBA78F00163E000886"
    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: "TVOC 상태"
    unique_id: air_monitor_tvoc_level
    state_topic: "sansa/data/7EF6FAA4016711EBA78F00163E000886"
    icon: mdi:chemical-weapon
    value_template: >-
      {% set data = value_json.sensor_data | base64_decode | from_json %}
      {% set level = data.tvoc.level %}
      {% set labels = {
        'excellent': '매우 좋음',
        'good': '좋음',
        'moderate': '보통',
        'unhealthy': '나쁨',
        'very_unhealthy': '매우 나쁨',
        'hazardous': '위험'
      } %}
      {{ labels.get(level, level) }}
    device: *air_monitor_device

  - name: "조도"
    unique_id: air_monitor_illuminance
    state_topic: "sansa/data/7EF6FAA4016711EBA78F00163E000886"
    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/7EF6FAA4016711EBA78F00163E000886"
    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

  - name: "배터리 상태"
    unique_id: air_monitor_battery_status
    state_topic: "sansa/data/7EF6FAA4016711EBA78F00163E000886"
    entity_category: diagnostic
    icon: mdi:battery-heart-variant
    value_template: >-
      {% set data = value_json.sensor_data | base64_decode | from_json %}
      {% set status = data.battery.status %}
      {% set labels = {
        'discharge': '방전 중',
        'discharging': '방전 중',
        'charge': '충전 중',
        'charging': '충전 중',
        'full': '충전 완료'
      } %}
      {{ labels.get(status, status) }}
    device: *air_monitor_device

  - name: "원본 Payload"
    unique_id: air_monitor_raw_payload
    state_topic: "sansa/data/7EF6FAA4016711EBA78F00163E000886"
    entity_category: diagnostic
    icon: mdi:code-json
    value_template: >-
      {{ value_json.timestamp }}
    json_attributes_topic: "sansa/data/7EF6FAA4016711EBA78F00163E000886"
    json_attributes_template: >-
      {% set data = value_json.sensor_data | base64_decode | from_json %}
      {{ {
        'device_id': value_json.device_id,
        'sensor_data': data,
        'signature': value_json.signature,
        'timestamp': value_json.timestamp,
        'version': value_json.version
      } | tojson }}
    device: *air_monitor_device

level 센서는 해당 센서가 없는 다른 묶음의 메시지를 받을 때 템플릿 평가가 실패할 수 있다. Home Assistant MQTT 센서는 value template 오류 시 기존 상태를 유지하므로 환경 센서 묶음과 공기질 센서 묶음이 번갈아 와도 마지막 정상 상태가 보존된다.

11. Home Assistant UI 자동화

Home Assistant의 자동화 생성 화면에서 YAML 편집 모드로 전환한 뒤 다음을 붙여 넣는다.

description: "에어모니터에 10초 주기 센서 데이터 보고를 요청합니다."
mode: single
triggers:
  - trigger: homeassistant
    event: start

  - trigger: time_pattern
    hours: "/12"

conditions: []

actions:
  - delay:
      seconds: 10

  - action: mqtt.publish
    data:
      topic: "sansa/command/7EF6FAA4016711EBA78F00163E000886"
      payload: '{"type":"12","up_itvl":"10","duration":"86400"}'
      qos: 0
      retain: true

동작 방식:

  • Home Assistant 시작 10초 후 명령 발행
  • 이후 12시간마다 명령 갱신
  • 장치에 10초 보고 간격을 요청함(실제 관찰 간격은 약 20초~1분으로 가변적이며 정확한 동작 여부 미확인)
  • 한 번 받은 명령은 24시간 유지
  • retained 명령이므로 장치가 재접속하면 즉시 다시 전달

12. 적용과 확인

  1. configuration.yamlmqtt.yaml 저장
  2. Home Assistant 개발자 도구에서 구성 검사
  3. Home Assistant 재시작
  4. 설정 → 장치 및 서비스 → MQTT 확인
  5. 에어모니터 장치 하나 아래 엔티티가 묶였는지 확인
  6. 자동화를 수동 실행하거나 장치를 완전히 재부팅
  7. MQTT broker 로그에서 data topic 발행 확인

최종 엔티티:

에어모니터
├─ 온도
├─ 습도
├─ 이산화탄소
├─ 이산화탄소 상태
├─ 극초미세먼지 PM1
├─ PM1 상태
├─ 초미세먼지 PM2.5
├─ PM2.5 상태
├─ 미세먼지 PM10
├─ PM10 상태
├─ TVOC
├─ TVOC 상태
├─ 조도
├─ 배터리
├─ 배터리 상태
└─ 원본 Payload

13. 문제 해결

MQTT에 장치 접속이 보이지 않음

  • USB만 분리하지 말고 내장 배터리까지 포함해 장치를 완전히 종료한다.
  • nslookup으로 MQTT 도메인이 내부 IP를 반환하는지 확인한다.
  • 공유기 tcpdump에서 DNS 응답과 TCP 1883 SYN을 확인한다.
  • MQTT 브로커가 LAN의 TCP 1883에서 실제로 수신 중인지 확인한다.

command topic만 보이고 data topic이 없음

  • command 메시지가 retained인지 확인한다.
  • 브로커 로그에 Sending PUBLISH to 7EF6...가 있는지 확인한다.
  • 장치를 완전히 재부팅한다.
  • command payload의 type, up_itvl, duration을 확인한다.

센서 일부만 한 번에 보임

정상 동작이다. 장치는 온도·습도 묶음과 공기질 묶음을 별도 메시지로 전송한다. mqtt.yaml의 숫자 센서는 해당 키가 없는 메시지에서 빈 값을 반환하여 이전 값을 유지한다.

SmartThings와 동시에 사용할 수 있는가

현재 DNS 우회 대상은 MQTT 도메인뿐이며 sansa.cleargrass.com HTTPS는 원래 서버로 유지한다. 따라서 SmartThings 클라우드 통신이 계속될 가능성이 높지만, 장기 동작과 펌웨어별 차이는 별도로 확인해야 한다.

14. 보안 주의사항

  • MQTT TCP 1883은 평문이므로 LAN 패킷을 볼 수 있는 사용자는 인증정보와 센서 값을 읽을 수 있다.
  • acm-mqtt-auth.pcap에는 MQTT 비밀번호가 포함될 수 있다.
  • PCAP, 비밀번호 파일, MQTT 비밀번호를 공개 Git 저장소에 커밋하지 않는다.
  • 익명 MQTT 브로커는 인터넷과 게스트 네트워크에 노출하지 않는다.
  • signature 값은 현재 로컬 센서 읽기에는 사용하지 않지만 원본 데이터의 일부이므로 불필요하게 공개하지 않는다.