본문 바로가기
Hardware/MCU(Arduino,ESP8266)

ESPHome 커스텀 컴포넌트 추가해서 Home Assistant에 설치

by lovey25 2023. 1. 29.
반응형

ESPHome 커스텀 컴포넌트가 필요했던 이유

ESPHome 처음 접했을 때가 한 3년 전인가 그랬던 거 같은데 그때도 참 대단한 프로젝트라고 생각을 했었습니다. 그런데 쉬지 않고 업데이트가 거듭되면서 시간이 지난 지금도 개선되는 내용을 보면서 놀라움을 금치 못하고 있습니다.  지원되는 컴포넌트만 해도 400가지가 넘고 사용성도 좋아져서 범용 솔루션이라고 할 수 있을 것 같습니다.

ESPHome의 다양한 컴포넌트들

ESPHome에 대한 경험이 많지는 않지만 그중에서도 유용하게 사용하는 프로젝트가 바로 ESP8266 모듈에 RS485 변환모듈을 연결해서 아파트 월패드 컨트롤러로 사용하는 건데요. 한동안 뭣도 모르면서 따라 하느라 고생 좀 했던 거라서 블로그에도 시리즈로 3편이나 포스팅을 했었더랬죠. (Home Assistant, 더샵 아파트 월패드 연동 1편 / 2편 / 3편)

그런데 월패드에서 기기들 간의 통신에 RS485 프로토콜을 사용하는데 이때만 해도 RS485 프로토콜을 ESPHome에서 지원해주지 않아서(있었지만 몰랐을지도 모르겠네요) ESPHome에다가 rs485 커스텀 컴포넌트를 추가해서 사용을 할 수밖에 없었습니다. 당연히 제가 그런 걸 할 능력은 안되고 어느 능력자분께서 공유해 주신 add-on을 사용했었죠. 이거 진짜 대박인 게 월패드에서 사용하는 RS485 통신에 필요한 모든 기능을 넣어서 완벽하게 만들어주셨습니다.

 

GitHub - greays/esphome: ESPHome is a system to control your ESP8266/ESP32 by simple yet powerful configuration files and contro

ESPHome is a system to control your ESP8266/ESP32 by simple yet powerful configuration files and control them remotely through Home Automation systems. - GitHub - greays/esphome: ESPHome is a syste...

github.com

그런데 이 분께서도 이 프로젝트가 주업이 아니실 테니 업데이트가 바로바로 되지 않을 때가 있습니다. 반면 ESPHome은 지속해서 개발이 진행 중인 프로젝트이기 때문에 예전에 몇 달 동안 rs485 커스텀 컴포넌트가 최신 ESPHome과 호환이 되지 않아서 업데이트를 할 수 없었던 경험이 있었습니다.

다행히 지금은 유지보수를 해 주셔서 최신 ESPHome 버전에서도 잘 사용을 할 수 있는 상태인데 머지않아 이런 일은 또 발생할 수 있기 때문에 그때를 대비해 저도 뭔가를 준비해야 할 것 같았습니다. 물론 이런 엄청난걸 첨부터 해보라면 엄두가 나지 않지만, 이미 다 차려진 밥상에서 수저 하나 더 놓는 거 정도는 할 수 있겠다 싶어서 ESPHome 공식 저장소를 포킹 해서 이분의 rs485 컴포넌트를 밀어 넣고 호환이 되지 않는 부분이 발생하면 땜빵으로 수정을 해서 사용하면서 능력자님의 업데이트를 기다리는 그런 전략입니다.

커스텀 컴포넌트 복사하기

일단 원조 ESPHome의 github 저장소를 포킹 해서 ESPHome 프로젝트 개발환경을 구축합니다. 개발환경 구축에 대한 내용은 이전 포스팅을 참고해 주세요.

 

ESPHome 개발환경 구축

ESPHome은 제가 참 좋아하는 IOT 설루션입니다. ESP8266 모듈과 함께 하면서 원래 아두이노로 하던 프로젝트까지도 모두 ESPHome으로 다시 뒤집어엎을 정도로 주로 사용하고 있습니다. 워낙 잘 만들어

kwonkyo.tistory.com

이제 "greays"님의 "esphome" 저장소에서 커스텀 컴포넌트인 "rs485"를 복사해 오겠습니다. git을 잘 사용하신다면 좀 더 편리한 방법이 있는지 모르겠지만 전 무식하게 그냥 복사했습니다. ㅡ.ㅡ;  rs485 컴포넌트의 위치는 "\esphome\esphome\components" 입니다.

rs485 컴포넌트 테스트

이제 복사된 컴포넌트가 현재 ESPHome 버전과 잘 호환이 되는지 테스트를 해보겠습니다. 프로젝트 폴더 안에 'tests'라는 이름의 폴더 안을 보면 테스트 스크립트가 여러 개 들어 있습니다. 그중에서 esphome 플랫폼용 스크립트인 "test3.yaml"파일을 사용합니다.

저는 rs485 컴포넌트로 현재 사용하고 있는 스크립트에서 중복되는 노드들 삭제하고 종류별로 하나씩 포함되도록 스크립트를 아래와 같이 수정했습니다.

---
esphome:
  name: $device_name
  comment: $device_comment
  build_path: build/test3-1

esp8266:
  board: d1_mini
  early_pin_init: true

substitutions:
  device_name: test3-1
  device_comment: test3-1 device
  min_sub: "0.03"
  max_sub: "12.0%"

api:
  port: 8000
  password: pwd

wifi:
  ssid: "MySSID"
  password: "password1"
# RS485 Component (for ttl to rs485 module)
#  - esp8266: UART0 (TX: GPIO1, RX: GPIO3)
#  - esp32: UART2 (TX: GPIO17, RX: GPIO16)
rs485:
  baud_rate: 9600 #Required
  data_bits: 8 #Option(default: 8)
  parity: 0 #Option(default: 0)
  stop_bits: 1 #Option(default: 1)

  rx_wait: 10 #Option(default: 10ms)  -> 수신 메시지 대기시간 (10ms 미만으로 수신된 메시지만 한 패킷으로 판단)
  tx_interval: 50 #Option(default: 50ms) -> 발신 메시지 전송 간격 (패킷 수신 후 50ms 대기 후 전송)
  tx_wait: 100 #Option(default: 100ms) -> 발신 메시지 Ack 대기시간
  tx_retry_cnt: 3 #Option(default: 3)     -> 발신 메시지 Ack 없을 경우 재시도 횟수
  prefix: [0xF7] #Option -> 값 세팅시 모든 수신 패킷 Check, 발신 패킷에 Append
  suffix: [0xEE] #Option -> 값 세팅시 모든 수신 패킷 Check, 발신 패킷에 Append

  checksum: True #Option(default: False) -> 체크섬 사용여부 (lambda 사용시 세팅 불필요)
  # checksum_lambda: |- #Option -> Default(CheckSum8 Xor) 체크섬 아닐 경우 직접 로직 구현(아래 값은 Default 로직임)
  #   // @param: const uint8_t *data, const unsigned short len
  #   // @return: uint8_t
  #   uint8_t crc = 0xF7; // data 변수에는 prefix 제외되어 있음
  #   for(num_t i=0; i<len; i++)
  #     crc ^= data[i];
  #   return crc;

  state_response: #Option -> 값 세팅시 response 패킷 수신 후에 명령 패킷 송신
    data: [0x04]
    offset: 3

  packet_monitor: #Option -> 패킷 모니터: Array 없으면 전체 출력, 있을 경우 or 조건 (logger level DEBUG 추천)
    - [0x0D, 0x01, 0x34] #0ffset:0
    #- data: [0x04]
    #  offset: 3

light:
  - platform: rs485
    name: "Livingroom1"
    device: [0x0b, 0x01, 0x19, 0x04, 0x40, 0x11, 0x00]
    state_on:
      offset: 7
      data: [0x01]
    state_off:
      offset: 7
      data: [0x02]
    command_on:
      data: [0x0b, 0x01, 0x19, 0x02, 0x40, 0x11, 0x01, 0x00]
      ack: [0x0b, 0x01, 0x19, 0x04, 0x40, 0x11, 0x01, 0x01]
    command_off:
      data: [0x0b, 0x01, 0x19, 0x02, 0x40, 0x11, 0x02, 0x00]
      ack: [0x0b, 0x01, 0x19, 0x04, 0x40, 0x11, 0x02, 0x02]
    command_state: [0x0b, 0x01, 0x19, 0x01, 0x40, 0x11, 0x00, 0x00] # 요청은 한곳에서만
    update_interval: 20s # 상태요청 주기

fan:
  - platform: rs485
    name: "Ventilation"
    device: [0x0c, 0x01, 0x2b, 0x04, 0x40, 0x11, 0x00]
    state_on:
      offset: 7
      data: [0x01]
    state_off:
      offset: 7
      data: [0x02]
    command_on:
      data: [0x0b, 0x01, 0x2b, 0x02, 0x42, 0x11, 0x01, 0x00]
      ack: [0x0c, 0x01, 0x2b, 0x04, 0x41, 0x2b, 0x04, 0x40, 0x11, 0x00, 0x01]
    command_off:
      data: [0x0b, 0x01, 0x2b, 0x02, 0x40, 0x11, 0x02, 0x00]
      ack: [0x0c, 0x01, 0x2b, 0x04, 0x40, 0x2b, 0x04, 0x40, 0x11, 0x00, 0x02]

switch:
  - platform: template
    name: doorlock
    turn_on_action:
      - rs485.write:
          data:
            [0x0e, 0x01, 0x1e, 0x02, 0x43, 0x11, 0x04, 0x00, 0x04, 0xff, 0xff]
          ack: [0x0c, 0x01, 0x1e, 0x04, 0x43, 0x11, 0x04, 0x00, 0x04]

# RS485 Climate
climate:
  # [거실 난방] 0x11
  # 상태 요청: 0xF7, 0x0B, 0x01, 0x18, 0x01, 0x45, 0x11, 0x00, 0x00, 0xB0, 0xEE
  # 켜짐 상태: 0xF7, 0x0D, 0x01, 0x18, 0x04, 0x45, 0x11, 0x00, (0x01, 0x1B, 0x17), 0xBE, 0xEE (상태, 현재온도, 설정온도)
  # 꺼짐 상태: 0xF7, 0x0D, 0x01, 0x18, 0x04, 0x45, 0x11, 0x00, (0x04, 0x1B, 0x17), 0xBB, 0xEE (상태, 현재온도, 설정온도)
  # 외출 상태: 0xF7, 0x0D, 0x01, 0x18, 0x04, 0x45, 0x11, 0x00, (0x07, 0x1B, 0x17), 0xB9, 0xEE
  # 켜짐 명령: 0xF7, 0x0B, 0x01, 0x18, 0x02, 0x46, 0x11, 0x01, 0x00, 0xB1, 0xEE
  #      ACK: 0xF7, 0x0D, 0x01, 0x18, 0x04, 0x46, 0x11, 0x01, 0x01, 0x1B, 0x17, 0xBC, 0xEE
  # 꺼짐 명령: 0xF7, 0x0B, 0x01, 0x18, 0x02, 0x46, 0x11, 0x04, 0x00, 0xB4, 0xEE
  #      ACK: 0xF7, 0x0D, 0x01, 0x18, 0x04, 0x46, 0x11, 0x04, 0x04, 0x1B, 0x17, 0xBC, 0xEE
  # 온도 조절: 0xF7, 0x0B, 0x01, 0x18, 0x02, 0x45, 0x11, (0x18), 0x00, 0xA7, 0xEE (온도 24도 설정)
  #      ACK: 0xF7, 0x0D, 0x01, 0x18, 0x04, 0x45, 0x11, (0x18), 0x01, (0x1A, 0x18), 0xA8, 0xEE
  - platform: rs485
    name: "Livingroom Heater"
    visual:
      min_temperature: 20 °C
      max_temperature: 30 °C
      temperature_step: 1 °C
    device: [0x0D, 0x01, 0x18, 0x04, 0x46, 0x11, 0x00]
    state_heat: #Option (난방모드, 냉방모드: state_cool, 자동모드: state_auto)
      offset: 7
      data: [0x01]
    state_off: #Required (끄기 상태)
      offset: 7
      data: [0x04]
    state_away: #Option (외출모드)
      offset: 7
      data: [0x07]
    state_current: #Required (현재온도 State, RS485 Sensor 설정 참고, sensor:로 대체 가능)
      offset: 8
      length: 1
      precision: 0
    state_target: #Required (설정온도 State)
      offset: 9
      length: 1
      precision: 0

    command_off: #Required (끄기 명령)
      data: [0x0B, 0x01, 0x18, 0x02, 0x46, 0x11, 0x04, 0x00]
      ack: [0x0D, 0x01, 0x18, 0x04, 0x46, 0x11, 0x04, 0x04]
    command_heat: #Option (난방모드 켜기)
      data: [0x0B, 0x01, 0x18, 0x02, 0x46, 0x11, 0x01, 0x00]
      ack: [0x0D, 0x01, 0x18, 0x04, 0x46, 0x11, 0x01, 0x01]
    command_away: #Option (외출 켜기)
      data: [0x0B, 0x01, 0x18, 0x02, 0x46, 0x11, 0x07, 0x00]
      ack: [0x0D, 0x01, 0x18, 0x04, 0x46, 0x11, 0x07, 0x07]
    command_temperature: !lambda |- #Required (온도 조절)
      // @param: const float x
      return {
               {0x0B, 0x01, 0x18, 0x02, 0x45, 0x11, (uint8_t)x, 0x00},
               {0x0D, 0x01, 0x18, 0x04, 0x45, 0x11, (uint8_t)x, 0x01}
            };
    command_state: [0x0B, 0x01, 0x18, 0x01, 0x46, 0x11, 0x00, 0x00] # 요청은 한곳에서만
    update_interval: 60s # 상태요청 주기

이제 test스크립트를 실행해서 수정한 test3.yaml 스크립트가 잘 컴파일되는지 확인합니다.

다행히 아직은 잘 컴파일이 되고 있습니다. 이제 커스텀 컴포넌트가 사용할 수 있는 상태임을 확인했으니 변경된 저장소를 커밋하고 포킹 한 github 저정소로 푸시합니다. 

ESPHome 기기에 커스텀 컴포넌트 적용하기기

지금부터는 Home Assistant에 설치된 ESPHome에서 커스컴 컴포넌트를 활용해 보겠습니다. Home Assistant에 깔려있는 ESPHome 패널을 열고 새로운 디바이스 만들기 해서 커스텀 컴포넌트를 적용할 디바이스를 추가하고 yaml 스크립트를 작성합니다. 

 

esphome:
  name: thesharp
  platform: ESP8266
  board: d1_mini

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  ap:
    ssid: "SOS_theSharp"
    password: !secret wifi_password

captive_portal:

logger:
  baud_rate: 9600
  level: INFO
  
api:
  password: !secret ota_password

ota:
  password: !secret ota_password

external_components:
  - source: github://coalcooker/esphome@master
    components: [rs485]
    refresh: 0s
    
// ---- 이하 생략 ----

이 스크립트에서 중요한 부분은 바로 26~29행인 exteran_components 섹션입니다. 'source' 옵션에는 방금 푸시했던 브랜치의 주소를 적어줍니다. 그리고 'components'에 커스텀 컴포넌트의 폴더명을 배열 형태로 적어줍니다. 만약 'components'옵션을 사용하지 않으면 사용가능한 모든 컴포넌트가 적용된다고 하네요. 그리고 마지막으로 'refresh'옵션을 "0s"로 추가해 줍니다. 'ESPHome에서는 esternal_components를 사용하면 필요한 컴포넌트의 소스를 다운로드하여서 로컬에 저장해 두고 사용한다고 합니다. 캐시 같은 건데 기본적으로 24시간이 적용되는 것 같습니다. 그래서 만약 커스컴 컴포넌트에 수정사항이 생겼더라도 'refresh' 옵션을 사용하지 않으면 수정전의 컴포넌트를 사용하게 됩니다. "0s"옵션은 캐시를 사용하지 않고 컴파일 때마다 커스텀 컴포넌트를 다운로드하도록 해 줍니다.

이제 ESPHome에서 스크립트를 컴파일해 보면 github에서 컴포넌트 정보 잘 가져오고 컴파일도 잘 되는 걸 확인할 수 있습니다.

마무리

앞으로 이런 머리 아픈 작업을 해야 할 일은 발생하지 않았으면 하는데 혹시나 긴급사태가 발생할 경우를 대비해서 또 헤매지 않기 위해서 힘들었던 과정을 기록해 둡니다. 그리고 혹시, 많지는 않겠지만 저와 비슷한 상황에 처하신 동지가 있으시다면 조금이나마 도움이 되면 좋겠습니다.

 

끝!

반응형

댓글