發布於

讓家裡的冷氣變聰明:遠端遙控與自動化 DIY

作者
  • avatar
    名稱
    Kevin Fan
    Twitter
ESP32 接上冷氣

某天滑脆(Threads)看到 @kunlin0813 分享他第一次自己焊接,就把公司的日立冷氣接進了 HomeKit,材料成本不到 100 元。原來滑脆也能滑到能動手做的東西——看完手很癢,決定把自家冷氣也弄上智慧家庭。

我家是 MAXE(萬士益)的分離式冷氣,平常只能拿遙控器在房間裡按。我真正想要的,是兩件原廠做不到的事:人在外面也能遠端開關,以及一些自動化——睡前定時關、關機後自動防霉吹乾這類冷氣預設不會做的事。原廠連網模組又貴、又得繞原廠雲端,於是乾脆自己用一片 ESP32 接管它。

這篇把整個過程記錄下來:硬體怎麼接、ESPHome 怎麼設、過程踩了哪些雷,以及接管之後真正好玩的自動化——包含最後一個看似簡單、結果意外燒腦的小謎題。

為什麼自己做(而不是買原廠模組)

原廠連網模組自己 DIY(本專案)
花費整套模組,不便宜幾個零件,便宜得多
雲端走雲端本地直連,不出門
自動化App 內建、有限Home Assistant 全自動化都能用
生態綁自家 AppApple Home/Google/Alexa 任選

拆開室內機控制盒,主板上印著 KFR-35G——這是美的(Midea)的 OEM 機種。換句話說,這台冷氣的 MCU「母語」就是 Midea 的 UART 協定。剛好 ESPHome 內建、社群成熟的 midea 元件就能直接跟它對話,不用自己逆向協定。先確認自己機種的母語協定,是整件事的第一步。

硬體:一片 ESP32-C3 + 一塊電平轉換板

需要的東西其實很少:

物品規格數量
ESP32-C3 SuperMini開發板1
BSS138 電平轉換板4 通道1
JST XH 2.54mm 帶線母頭4-pin、20cm1

冷氣控制盒上有個 CN3 WIFI 接頭(JST XH 2.54mm、4-pin),規格是 5V TTL、UART 9600 8N1。問題是 ESP32-C3 的 GPIO 是 3.3V,直接把 5V 灌進去會慢性損壞甚至燒掉,所以中間要墊一塊 BSS138 雙向電平轉換板做 5V ↔ 3.3V 轉換。

CN3 接線

關於電容:很多教學會建議在 ESP 的 5V/GND 之間並一顆 470µF 電解電容,避免壓縮機啟動瞬間電壓跌落害 ESP brown-out 重開。我實測沒裝也很穩,所以目前沒裝;如果你的環境會掉電,再並一顆上去(注意極性,接反會爆)。

踩雷①:只有三用電表、沒示波器,分辨 pin 踩了不少雷

CN3 雖然只有 4 根 pin,但要分清楚哪根是 +5V、GND、TX、RX 沒想像中簡單——我手邊只有一支簡易電子三用電表,沒有示波器,只能量靜態電壓去猜。

第一個雷:這台的 pin 排列跟 Midea 慣例完全相反。實測結果是 pin1 = GND、pin4 = +5V(慣例是 pin1 = +5V)。照網路上的 pinout 接,鐵定接錯——一定要自己量。

第二個、也是最坑的一個:量起來最乾淨、紋風不動的那根 5V,其實不是電源。

  • 真正的 +5V,量起來會「微微抖動」(有 ripple)——因為冷氣 MCU 在運作、週期性吃電。
  • 那根穩定到漂亮的 5V,反而是被 pull-up 的訊號腳,背後只接著一顆 10kΩ 電阻,最多供 ~0.5mA。你接上去,ESP 的 LED 會亮(騙過你),但 WiFi 一啟動需要電流,立刻 brown-out 重開。

我就是差點被那根「漂亮的 5V」騙走。記住一句:會抖的才是電源。

踩雷②:TX/RX 分不出來?用猜的

兩根訊號腳靜態都是被 pull-up 的 5V,電表根本分不出誰是 TX、誰是 RX。最省事的做法不是硬分,而是:先猜一個方向接,燒上去看 log;如果一直 Response timeout,就把 YAML 裡的 tx_pinrx_pin 對調、OTA 重燒即可——不用拆線,而且 TX/RX 接反不會燒壞,只是收不到資料。

⚠️ 安全提醒:接上冷氣之後,絕對不要再把 ESP 的 USB 接到電腦。冷氣 5V 的地線是參考 220V 火線的,萬一開關電源絕緣擊穿,地線會直接帶 220V,USB 線就成了 220V 直通電腦主機板的路徑——輕則炸主板、重則觸電。所有韌體更新一律走 OTA。

韌體:ESPHome 的 midea 元件

ESPHome 設定我拆成「共用檔 + 各台入口檔」:共用檔放所有設定,每台冷氣各有一個小入口檔,用 substitutions 帶入名字、再 !include 共用檔。這樣兩台(客廳、臥室)共用同一份設定。

入口檔長這樣:

# aircon-living.yaml — 客廳那台
substitutions:
  device_name: maxe-aircon-living
  friendly_name: 客廳冷氣

packages:
  common: !include maxe-aircon-common.yaml

共用檔的關鍵片段:

esp32:
  board: esp32-c3-devkitm-1
  variant: ESP32C3 # ESP32-C3 顯式寫這行,連線比較穩

# Logger 走 USB Serial/JTAG,把 UART0 留給冷氣
logger:
  hardware_uart: USB_SERIAL_JTAG

uart:
  tx_pin: GPIO21
  rx_pin: GPIO20
  baud_rate: 9600

climate:
  - platform: midea
    id: ac
    name: ${friendly_name}
    period: 1s
    beeper: true # 收到指令時冷氣會嗶一聲(後面會聊這個)
    supported_modes:
      - COOL
      - DRY
      - FAN_ONLY # 送風,防霉吹乾要用
      # ...(HEAT / HEAT_COOL 視機種)

# 接到冷氣後一律走 OTA,不要再插 USB
ota:
  - platform: esphome
    password: !secret ota_password

幾個重點:

  • logger: hardware_uart: USB_SERIAL_JTAG:ESP32-C3 的 log 改走內建的 USB Serial/JTAG,把 UART0(GPIO20/21)整條留給冷氣,不會互搶。
  • beeper: true:每次冷氣收到指令會嗶一聲,debug 階段很有用(聽得到指令有沒有送到)。這個後來變成一個有趣的問題(見最後一段)。
  • HomeKit 的恆溫器只有 關/冷/暖/自動,沒有「送風」「除濕」。我另外加了兩個 template switch 補位,這樣在 Apple 家庭 App 裡也能單獨開送風/除濕。

收訊小提醒:ESP32-C3 內建的 PCB 天線如果收訊弱、容易斷線,最簡單的解法是換一塊有外接天線座的板子——我後來改用 XIAO ESP32-C3、把附的天線接上去就很穩了。

Home Assistant 跑在哪:N100 + Proxmox + HAOS

我的 Home Assistant 不是裝在樹莓派,而是跑在一台 N100 迷你電腦上:先裝 Proxmox VE(PVE),再在上面開一個 HAOS(Home Assistant OS) 虛擬機。

為什麼用 HAOS 而不是 Container/Core 版?因為裝 add-on 最方便——HACS、各種官方/社群 add-on 點一點就裝好,不用自己搞 Docker。N100 夠力又省電,順便還能在 PVE 上跑別的服務。

在 PVE 上安裝 HAOS 我參考這支影片:How to install Home Assistant OS on Proxmox

冷氣的 ESP 燒好、上線後,Home Assistant 會自動發現它,climate 實體就出現了。接下來才是真正好玩的部分。

接進 HA 之後,真正好玩的是自動化

硬體只是把冷氣「接上網」,價值其實在自動化。以下都做在 Home Assistant 的 package(YAML)裡,跟 ESPHome 無關。entity_id 用 placeholder(climate.living 之類)示意。

自動防霉吹乾

冷氣關機後蒸發器是濕的,悶著容易發霉。所以我做了:只要冷氣從 cool/dry 被關掉(包含用遙控器關),先自動轉送風吹乾 N 分鐘,再真正關機。

- alias: 客廳冷氣 自動防霉-關機前吹乾
  trigger:
    - platform: state
      entity_id: climate.living
      from: [cool, dry]
      to: "off"
  condition:
    - condition: state
      entity_id: input_boolean.living_antimold_enabled
      state: "on"
  action:
    - service: climate.set_hvac_mode
      target: { entity_id: climate.living }
      data: { hvac_mode: fan_only }
    - service: timer.start
      target: { entity_id: timer.living_fan_dry }
      data:
        duration: "{{ (states('input_number.fan_dry_minutes')|float*60)|int }}"

吹乾倒數結束,再有一條把它真正關掉。

睡眠定時

設定 N 小時 → 到時關機。關機這一下會被上面的「自動防霉」接手吹乾,所以睡前定時關=定時關 + 自動吹乾,一條龍。

踩雷③:殘留的 timer 會偷偷關機

這類「倒數 + 自動關」最容易出的 bug:吹乾/睡眠倒數還在跑時,我手動把冷氣切回冷氣模式(接手了),結果舊的倒數時間到,照樣把冷氣關掉——你會覺得「我明明在吹冷氣,怎麼自己關了?」。

解法是補上幾條「中止」自動化:偵測到你手動接手(climate 狀態變了)或手動關機,就把殘留的倒數 timer 取消掉;而且倒數結束、要關機前,再確認一次冷氣狀態真的還在該關的狀態才動手。邊界顧好,這套自動化才不會反過來惹人厭。

壓軸:怎麼讓「自動化靜音、自己按還是嗶」

還記得 beeper: true 嗎?冷氣每收到一個指令會嗶一聲。白天自己操作時,這聲嗶是很好的回饋;但半夜睡眠定時自動關機、自動吹乾也會嗶——這就很吵。

我要的效果是:自動化發的指令靜音,自己(或遙控器)手動按的照嗶。

ESPHome 的 midea 元件可以在執行期切換 beeper(midea_ac.beeper_on / beeper_off),我把它做成一個 switch。最直覺的版本(v1)是 dashboard 放一個總開關,睡前手動關、早上開回——最低成本,但要手動。

進階版(本篇重點)是「依來源靜音」:每一條會送指令的自動化,在送指令前先把 beeper 關掉,事後再還原。 聽起來簡單,但這裡藏了一個 race condition。

那個 race condition

睡眠定時到時關機(sleep_finished)這一下,會連鎖觸發自動防霉(antimold)去轉送風。也就是兩條自動化幾乎同時在動。如果每條都「關 beeper → 送指令 → 開回 beeper」,那麼 sleep_finished 開回 beeper 的瞬間,剛好撞上 antimold 要送的「轉送風」指令——於是那聲嗶就漏了出來。

解法:debounce 還原 + 尊重手動關

我把「還原 beeper」從每條自動化裡抽出來,改成一條獨立的還原自動化

當「睡眠倒數」和「吹乾倒數」兩個 timer 都閒置滿 10 秒,才把 beeper 開回去。

這 10 秒的 debounce 剛好吸收掉 sleep_finished → antimold 之間那段不到一秒的空檔,整條連鎖跑完、塵埃落定後才還原,就不會漏嗶。

還有一個我想要的行為:如果是我自己手動把 beeper 關著的,自動化跑完不應該擅自幫我開回來。 為此加了一個隱藏旗標 input_boolean.living_beeper_restore_pending

  • 靜音時:只有當 beeper 原本是開的,才標記旗標、再關掉。
  • 還原時:只有旗標有立才還原。

所以你自己關的 beeper(沒立旗標)永遠不會被自動化打開。

靜音片段(放在每條送指令的自動化前面):

- if:
    - condition: state
      entity_id: switch.living_beeper
      state: "on"
  then:
    - service: input_boolean.turn_on
      target: { entity_id: input_boolean.living_beeper_restore_pending }
    - service: switch.turn_off
      target: { entity_id: switch.living_beeper }
    - delay: "00:00:01" # 等 ESP 套用 flag,指令封包才會帶「不嗶」

還原自動化:

- alias: 客廳冷氣 beeper 還原(自動指令後)
  trigger:
    - platform: state
      entity_id: timer.living_sleep
      to: "idle"
      for: "00:00:10"
    - platform: state
      entity_id: timer.living_fan_dry
      to: "idle"
      for: "00:00:10"
  condition:
    - condition: state
      entity_id: input_boolean.living_beeper_restore_pending
      state: "on"
    - condition: state
      entity_id: timer.living_sleep
      state: idle
    - condition: state
      entity_id: timer.living_fan_dry
      state: idle
  action:
    - service: switch.turn_on
      target: { entity_id: switch.living_beeper }
    - service: input_boolean.turn_off
      target: { entity_id: input_boolean.living_beeper_restore_pending }

最後的行為:

  • 你沒動(beeper 開著)+ 自動化關機/吹乾 → 全程靜音,週期結束後自動還原成開。
  • 你自己把 beeper 關著 → 自動化照樣靜音,週期結束維持關,不會被偷開。
  • 你/遙控器手動操作 → 照嗶。
Home Assistant 主控板

心得

  1. 低成本、全本地不靠雲端,而且 Home Assistant 的自動化全都能用——CP 值很高。
  2. 硬體最大的雷是電源判斷(會抖的才是 +5V)和 pinout(一定要自己量,別信慣例)。
  3. 真正的價值在自動化;而自動化的魔鬼在邊界——殘留的 timer、連鎖觸發的 race condition,這些不處理,自動化反而會變成困擾。
  4. 完整程式碼(ESPHome + Home Assistant package)開源在 GitHub:kevinypfan/esp32-aircontroller

如果你家也有 Midea OEM 的冷氣,歡迎照著玩玩看,也歡迎分享你的接法與改良!