IOT

[HomeAssistant] openwrt를 활용하여 코콤 월패드 TCP 데이터 dump 및 활용(입차, 주차 알림)

오리야호 2023. 10. 3. 11:22
반응형


이전에 소개해 드렸던 tcpdump + python 조합의 입차, 주차 위치 알림을 tcpdump + mosquitto_pub 방식으로 개선해 보았습니다.
 
이 글에서 다루지 않지만 월패드에 openwrt 기반 라우터 연결과 몇 가지 작업이 선행되어야 합니다.
 
코콤 월패드 TCP 데이터 스니핑으로 입차/주차알림 사례 (OpenWRT + Python)

 

코콤 월패드 TCP 데이터 스니핑으로 입차/주차알림 사례 (OpenWRT + Python)

대한민국 모임의 시작, 네이버 카페

cafe.naver.com

https://cafe.naver.com/koreassistant/9672

 

코콤 월패드 TCP 분석 1탄 (OpenWRT + tcpdump)

대한민국 모임의 시작, 네이버 카페

cafe.naver.com

 
https://cafe.naver.com/koreassistant/9811

 

코콤 월패드 TCP 분석 2탄 (패킷 살펴보기)

대한민국 모임의 시작, 네이버 카페

cafe.naver.com

 
기존 방식은 tcpdump 데이터를 pcap 파일로 저장 후 별도의 VM에서 데이터 처리를 하다 보니 여러 가지 단점이 존재 했는데요.

  • 기능의 효과 대비 복잡도가 높았고, 관리 포인트가 늘어났습니다.
  • openwrt 장비의 용량이 작다 보니 VM에서 NFS도 설정해야 했고 무선 연결이 끊어질 경우를 위해 별도의 처리도 필요했고요.

openwrt에서 tcpdump 데이터를 직접 mqtt 서버에 publish 하는 방법을 왜 그 당시에 생각하지 못했을까요 ㅎㅎ
 
HA 까페 꼬르륵님이 작성하신 글을 보고 mosquitto_publish를 활용하는 아이디어를 얻어 간소화된 방식으로 다시 작업한 내용입니다.
 
https://cafe.naver.com/koreassistant/14516

 

구코콤 엘리베이터 호출 및 층수 디스플레이 추가 성공기(part 2)

대한민국 모임의 시작, 네이버 카페

cafe.naver.com

 
openwrt 설정에 관련된 부분은 여기서 다루지 않습니다.
 

tcpdump 데이터를 mqtt 로 발행하기

tcpdump -i br-wpad -x -l '(tcp or udp) and (src 출발지 or dst 목적지) and not (src 10.254.254.2 or dst 10.254.254.2)' | awk '/0x/{line=""; for(i=2; i<=NF; i++) line=line $i; printf "%s", line} /^[0-9]/{if(line) {print ""; line=""}} END{if(line) print ""}' | while read line; do mosquitto_pub -h "mqtt ip 주소" -t "발행할 topic" -u "계정" -P "비번" -r -m "$line"; done

 
위 명령은 파이프( | ) 로 3개의 명령이 한번에 처리 됩니다.

  1. tcpdump : tcp데이터를 필터링하여 원하는 데이터만 추출
  2. awk : tcpdump에서 추출된 명령어를 하나로 병합
  3. mosquitto_pub : mqtt 서버로 publish

 

1. tcpdump 관련 명령

  • 옵션 설명
    • x : 패킷의 내용을 16진수로 출력
    • X : 패킷의 내용을 16진수와 ASCII로 함께 출력 (모니터링할 때 가끔 사용)
    • l : 패킷을 캡처하자마자 바로 출력하도록 하는 옵션
  • br-wpad : tcp 데이터를 추출할 네트워크 인터페이스 (제 경우는 이전 글에 br-wpad로 작업)
  • tcp or udp ~~~ 조건 : tcp 데이터 중 필요한 패킷만 필터링하는 조건
    • 출발지는 세대 ip, 목적지는 단지 서버 ip 입니다.
    • 10.254.254.2 아이피는 월패드가 부팅될 때 영상, 음성 인증과 관련된 거라 제외하였습니다.
    • 필터링 조건은 세대마다, 개인마다 다르게 작성될 것 같습니다.

2. awk 관련 명렁

  • awk 이후~~ | while read line; 까지 : -x 옵션을 사용하면 16바이트 단위로 줄바꿈과 함께 출력되므로 전체 패킷을 합치는 작업이 필요했습니다. awk 는 텍스트 패턴 스캔과 처리하는 명령어 입니다.

(전등 관련 샘플 패킷, 아래 이미지 처럼 쪼개지는걸 합쳐서 mqtt로 보내기 위해 awk를 활용)
 

3. mosquitto_pub

  • r : 이 옵션은 메시지를 retain(보관) 하도록 설정. MQTT 브로커에 마지막으로 보낸 메시지를 저장해 두고, 새로운 구독자가 연결되었을 때 그 메시지를 받게 하려면 -r 옵션을 사용
  • l : 이 옵션은 mosquitto_pub가 표준 입력으로부터 여러 줄의 메시지를 읽을 수 있게 함. tcpdump의 출력이 여러 줄이면, 이를 각각의 메시지로 publish
  • h : mqtt 서버 ip
  • t : 발행할 토픽
  • u : 로그인 계정
  • P : 로그인 비번
  • m : 이 옵션 뒤에 메세지 내용을 작성

 

주의사항. wireshark의 데이터와 tcpdump -x 옵션의 데이터 차이점

 
wireshark로 확인하는 데이터와 tcpdump -x 옵션으로 저장하거나 mqtt로 발행한 데이터 차이가 있습니다.
 
wireshark 에서는 헤더 정보 전체를 포함하여 보여주고 있고, tcpdump의 -x 옵션은 OSI 계층 중 데이터링크 계층은 제외되고 추출됩니다.
 
tcpdump 데이터를 mqtt로 발생하면 45 00 ~~~ 부터 시작되니 문자열 index 처리 시 wireshark 기준으로 작업하면 안됩니다.
 

 

mqtt 로그를 별도 파일로 저장하고 싶을 경우 (디버깅용)

$ mosquitto_sub -h [MQTT_BROKER_HOST] -t [TOPIC] -u 계정 -P 비번 -v >> file.log

 
mqtt서버에서 발행된 메세지를 file.log로 저장하도록 하는 명령어 입니다.
 

MQTT 발행 데이터 확인

 
mqtt explorer를 활용하면 데이터 확인이 편합니다.
 
센서값이 제대로 처리 되는지 확인할 때 미리 기록해 둔 TCP 데이터로 발행도 가능합니다.
 

 
 

openwrt에서 부팅 시 실행되도록 처리하기

 
꼬르륵님 처럼 startup ui 에서 간단하게 처리 해도 됩니다. (부팅 후 딜레이를 줘서 명령어를 실행하는 방식)
 
저는 딜레이 대신 무선 연결 시 tcpdump 파일 기록과 mqtt 발행 2가지를 백그라운드에서 실행하도록 처리 하였습니다.
 
/etc/hotplug.d/iface/21-tcpdump-processor 파일 생성 후 아래 코드 참고

!/bin/sh
[ "$DEVICE" == "lo" ] && exit 0

#/usr/bin/logger hotplug.d iface : ACTION=$ACTION INTERFACE=$INTERFACE DEVICE=$DEVICE

case "$ACTION" in
  ifup)
    if [ "$DEVICE" == "wlan0" ]; then
      # TCP data to mqtt publish
      tcpdump -i br-wpad -x -l '(tcp or udp) and (src 출발지 or dst 목적지) and not (src 10.254.254.2 or dst 10.254.254.2)' | awk '/0x/{line=""; for(i=2; i<=NF; i++) line=line $i; printf "%s", line} /^[0-9]/{if(line) {print ""; line=""}} END{if(line) print ""}' | while read line; do mosquitto_pub -h "mqtt ip 주소" -t "발행할 topic" -u "계정" -P "비번" -r -m "$line"; done &
    fi
    ;;
  ifdown)
    if [ "$DEVICE" == "wlan0" ]; then
      kill -9 `ps | grep 'tcpdump' | awk '{print $1}'`
    fi
    ;;
esac

 
무선 연결이 종료되면 tcpdump 명령어를 kill 처리되고 다시 연결되면 tcpdump & awk & mosquitto_pub 명령이 백그라운드로 실행됩니다.
 
 

HA 에서 센서 만들기

 
패키지를 만들어서 kocom tcp 데이터 처리 관련만 따로 모았습니다.
 
HA에서 사용되는 Jinja 2에서는 16진수 데이터를 ASCII로 변환해 주는 함수가 없어서 조금 지저분하게 코드가 처리 됩니다.
 
여기서 소개 해드리는 센서는 마지막으로 입차한 차량과 주차한 차량 정보를 센서로 기록하는 방법입니다.
 
특정 차량 단위로 기록하고 싶을 경우 별도로 만들어줘야 합니다.
 
Last Entry Car Plate Number 센서 예를 들면 target_data에서 수신된 데이터로 target_data[8:12] 값이 입차일 경우 차랑 변호를 센서에 넣는 과정입니다.
 
차량번호가 1234일 경우 31 32 33 34 16진수 데이터가 202~210번 째 위치에 있을 텐데 이것을 ASCII로 변환하여 1234 값을 센서에 반영해 주는 템플릿 예시 입니다.
 
hex to ASCII 변환 사이트를 활용하면 쉽게 확인해 볼 수 있습니다.
 
16 진수에서 ASCII로 | 16 진수에서 텍스트 문자열로 변환기
 

 

16 진수에서 ASCII로 | 16 진수에서 텍스트 문자열로 변환기

16 진수-ASCII 텍스트 변환기 접두사 / 접미사 / 구분 기호와 함께 16 진수 바이트를 입력하고 변환 버튼을 누릅니다 (예 : 45 78 61 6d 70 6C 65 21) : ASCII에서 16 진수로 변환 ► ASCII 텍스트 인코딩은 각 문

www.rapidtables.org

 

# TCP 헤더 제외하고 데이터만 사용

# data[8:12]     입차 : 6e00
# data[202:210]  입차패킷 번호판
# data[160:188]  입차패킷 시간
# data[8:12]     주차 : fd00
# data[56:64]    주차패킷 번호판
# data[88:106]   주차위치

mqtt:
  - sensor:
      - name: "Kocom TCP Data"
        state_topic: "kocom_tcp"
        unique_id: "kocom_tcp_data"
        value_template: "'received'"
        json_attributes_topic: "kocom_tcp"
        json_attributes_template: '{"data": "{{ value[80:] }}"}'

template:
  - sensor:
      - name: "Last Entry Car Plate Number"
        state: >-
          {% set target_data = state_attr('sensor.kocom_tcp_data', 'data') %}
          {% if target_data is not none and target_data[8:12] == '6e00' %}
            {% set line = target_data[202:210] %}
            {% set chars = " !'#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~" %}
            {% set ns = namespace(value='') %}
            {% for i in range(0, line | length, 2) %}
              {% set hex_chunk = line[i:i+2] %}
              {% set decimal_value = hex_chunk | int('', 16) %}
              {% set char_index = decimal_value - 32 %}
              {% set char = chars[char_index] if char_index >= 0 and char_index < chars | length else undefined %}
              {% if char %}
                {% set ns.value = ns.value ~ char %}
              {% endif %}
            {% endfor %}
            {{ ns.value if ns.value else 'unavailable' }}
          {% else %}
            {{ states('sensor.last_entry_car_plate_number') }}
          {% endif %}

      - name: "Last Car Entry Time"
        device_class: "timestamp"
        state: >-
          {% set target_data = state_attr('sensor.kocom_tcp_data', 'data') %}
          {% if target_data is not none and target_data[8:12] == '6e00' %}
            {% set line = target_data[160:188] %}
            {% set chars = " !'#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~" %}
            {% set ns = namespace(value='') %}
            {% for i in range(0, line | length, 2) %}
              {% set hex_chunk = line[i:i+2] %}
              {% set decimal_value = hex_chunk | int('', 16) %}
              {% set char_index = decimal_value - 32 %}
              {% set char = chars[char_index] if char_index >= 0 and char_index < chars | length else undefined %}
              {% if char %}
                {% set ns.value = ns.value ~ char %}
              {% endif %}
            {% endfor %}
            {% if ns.value | length >= 14 %}
              {{ strptime(ns.value, '%Y%m%d%H%M%S') | as_local }}
            {% else %}
              unavailable
            {% endif %}
          {% else %}
            {{ states('sensor.last_car_entry_time') }}
          {% endif %}

      - name: "Last Parked Car Plate Number"
        state: >-
          {% set target_data = state_attr('sensor.kocom_tcp_data', 'data') %}
          {% if target_data is not none and target_data[8:12] == 'fd00' %}
            {% set line = target_data[56:64] %}
            {% set chars = " !'#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~" %}
            {% set ns = namespace(value='') %}
            {% for i in range(0, line | length, 2) %}
              {% set hex_chunk = line[i:i+2] %}
              {% set decimal_value = hex_chunk | int('', 16) %}
              {% set char_index = decimal_value - 32 %}
              {% set char = chars[char_index] if char_index >= 0 and char_index < chars | length else undefined %}
              {% if char %}
                {% set ns.value = ns.value ~ char %}
              {% endif %}
            {% endfor %}
            {{ ns.value if ns.value else 'unavailable' }}
          {% else %}
            {{ states('sensor.last_parked_car_plate_number') }}
          {% endif %}

      - name: "Last Parked Car Location"
        state: >-
          {% set target_data = state_attr('sensor.kocom_tcp_data', 'data') %}
          {% if target_data is not none and target_data[8:12] == 'fd00' %}
            {% set line = target_data[88:106] %}
            {% set chars = " !'#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~" %}
            {% set ns = namespace(value='') %}
            {% for i in range(0, line | length, 2) %}
              {% set hex_chunk = line[i:i+2] %}
              {% set decimal_value = hex_chunk | int('', 16) %}
              {% set char_index = decimal_value - 32 %}
              {% set char = chars[char_index] if char_index >= 0 and char_index < chars | length else undefined %}
              {% if char %}
                {% set ns.value = ns.value ~ char %}
              {% endif %}
            {% endfor %}
            {{ ns.value if ns.value else 'unavailable' }}
          {% else %}
            {{ states('sensor.last_parked_car_location') }}
          {% endif %}

      - name: "Last Parking Time"
        device_class: "timestamp"
        state: >-
          {% set target_data = state_attr('sensor.kocom_tcp_data', 'data') %}
          {% if target_data is not none and target_data[8:12] == 'fd00' %}
            {{ now() | as_local }}
          {% else %}
            {{ states('sensor.last_parking_time') }}
          {% endif %}

 

대시보드에 센서값 확인

 
차량이 2대 이상이고 가족들이 번갈아가면서 타야할 경우 HA에서 주차 위치를 센서로 만들어두면 편리합니다.
 

 
이렇게 확인이 되면 자동화를 통해 알림이나 TTS 로 활용될 수 있습니다.
 
 
 
 
 

반응형