Liora ChenAtom Yang
Published

Automated Cat House

Create an automated cat feeder that can also monitor the food level and track the environment (e.g., temperature and humidity)

IntermediateFull instructions provided213
Automated Cat House

Things used in this project

Hardware components

Seeed Studio XIAO ESP32S3 Sense
Seeed Studio XIAO ESP32S3 Sense
×1
Seeed Studio XIAO Expansion Board
Seeed Studio XIAO Expansion Board
×1
LED Driver Board for Seeed Studio XIAO
LED Driver Board for Seeed Studio XIAO
×1
Seeed Studio IoT Button
×1
Grove - Temperature & Humidity Sensor V2.0
×1
XIAO 7.5'' ePaper Panel
Seeed Studio XIAO 7.5'' ePaper Panel
×1
Grove-Ultrasonic-Distance-Sensor
×1
MG946R High-Torque Servo
×1

Software apps and online services

Home Assistant
Home Assistant

Story

Read more

Schematics

hardware connection

Code

epaper visualization

YAML
esphome:
  name: epaper-feeder
  friendly_name: ePaper-feeder

esp32:
  board: esp32-c3-devkitm-1
  framework:
    type: esp-idf

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "JYWHpAQ5Ow52vE67jS05EucTYTXaAr7/Q5iWKHgjF0s="

ota:
  - platform: esphome
    password: "6cf194616fb172a27af2a245a2ef3a63"

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

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Epaper-Feeder Fallback Hotspot"
    password: "JCTUr6xrDaGs"

captive_portal:

# ========= 传感器:从 HA 拉取三个值(把 entity_id 换成你的)=========
sensor:
  - platform: homeassistant
    id: s_fill
    entity_id: sensor.seeed_feeder_feeder_fill_pct
    internal: true
    on_value: [ script.execute: refresh_epd ]
  - platform: homeassistant
    id: s_temp
    entity_id: sensor.seeed_feeder_feeder_temp_c
    internal: true
    on_value: [ script.execute: refresh_epd ]
  - platform: homeassistant
    id: s_rh
    entity_id: sensor.seeed_feeder_feeder_rh
    internal: true
    on_value: [ script.execute: refresh_epd ]

# 同步 HA 时间(可选,仅用来显示“Updated …”)
time:
  - platform: homeassistant
    id: ha_time

# 字体
font:
  - file: "gfonts://Inter@900"
    id: f_title
    size: 60

  - file: "gfonts://Inter@700"
    id: f_label
    size: 38

  # 数值:百分比用更粗更大
  - file: "gfonts://Inter@800"
    id: f_value_pct
    size: 72

  # 数值:温度主体稍小,避免撞圈
  - file: "gfonts://Inter@800"
    id: f_value_temp
    size: 64

  - file: "gfonts://Inter@700"
    id: f_unit_c       # C 的字号
    size: 36

  - file: "gfonts://Inter@700"
    id: f_unit_deg     # ° 更小,做上标
    size: 24


# 图片(放到 /config/esphome/images/)
image:
  - file: /config/esphome/image/cat1.png
    id: icon_cat
    type: BINARY
    resize: 56x56

# 刷屏脚本:对 on_value / 连接等触发做轻微节流
script:
  - id: refresh_epd
    mode: restart
    then:
      - delay: 150ms
      - component.update: epd

# SPI(按 XIAO ePaper 板载连法)
spi:
  clk_pin: GPIO8
  mosi_pin: GPIO10

# 显示(7.5" ePaper)
display:
  - platform: waveshare_epaper
    id: epd
    cs_pin: GPIO3
    dc_pin: GPIO5
    reset_pin: GPIO2
    busy_pin:
      number: GPIO4
      inverted: true
    model: 7.50inv2p      # ★若出现黑底白字,则保持此行;若颜色相反,把 BG/INK 两行对调并把型号改为 7.50inv2 或 7.50inV2
    rotation: 
    update_interval: 30s
    lambda: |-
      // ===== 颜色策略(inv2p 反色:代码黑底白字 → 视觉白底黑字)=====
      const auto BG  = Color::BLACK;  // 非反色面板改为 Color::WHITE
      const auto INK = Color::WHITE;  // 非反色面板改为 Color::BLACK
      const int W = 800, H = 480;
      it.fill(BG);

      // ===== 顶部:左右猫头 + 居中标题 =====
      const int CAT_W = 56, CAT_H = 56;
      it.image(28, 20, id(icon_cat));                  // 左猫头
      it.image(W - 28 - CAT_W, 20, id(icon_cat));      // 右猫头
      it.printf(W/2, 22, id(f_title), INK, TextAlign::TOP_CENTER, "XIXI & DIDI's HOME");

      // ===== 三列布局参数 =====
      const int CY  = 320;   // 圆心 Y
      const int R   = 112;   // 外半径(略放大)
      const int T   = 22;    // 圆环厚度
      const int CX1 = 150;   // Feeder
      const int CX2 = 400;   // Temperature
      const int CX3 = 650;   // Humidity

      // 标题(圆上方)
      it.printf(CX1, 150, id(f_label), INK, TextAlign::TOP_CENTER, "Feeder");
      it.printf(CX2, 150, id(f_label), INK, TextAlign::TOP_CENTER, "Temperature");
      it.printf(CX3, 150, id(f_label), INK, TextAlign::TOP_CENTER, "Humidity");

      // 画粗圆环:外圆填 INK,再用 BG 挖内圆
      auto draw_ring = [&](int cx, int cy, int r, int thick){
        it.filled_circle(cx, cy, r, INK);
        it.filled_circle(cx, cy, r - thick, BG);
      };
      draw_ring(CX1, CY, R, T);
      draw_ring(CX2, CY, R, T);
      draw_ring(CX3, CY, R, T);

      // ===== 圆心数值 =====
      char buf[16];

      // Feeder(%)
      if (isnan(id(s_fill).state)) snprintf(buf, sizeof(buf), "--");
      else                        snprintf(buf, sizeof(buf), "%.0f%%", id(s_fill).state);
      it.printf(CX1, CY, id(f_value_pct), INK, TextAlign::CENTER, "%s", buf);

      // ===== Temperature:数值居中 + “°C”组合单位(不撞圈)=====
      const int inner = R - T;  // 内圆半径
      char tnum[12];
      if (isnan(id(s_temp).state)) snprintf(tnum, sizeof(tnum), "--");
      else                        snprintf(tnum, sizeof(tnum), "%.1f", id(s_temp).state);

      // 1) 数值居中
      it.printf(CX2, CY + 2, id(f_value_temp), INK, TextAlign::CENTER, "%s", tnum);

      // 2) 单位两字符叠放(Inter 有°,没有℃)
      const int ux = CX2 + int(inner * 0.58f);
      const int uy = CY  - int(inner * 0.28f);
      const int dx = 12;                    // ° 到 C 的水平距离(10~14 可微调)
      it.printf(ux,        uy - 5, id(f_unit_deg), INK, TextAlign::CENTER,      "\xC2\xB0"); // 小一号的 °
      it.printf(ux + dx,   uy + 1, id(f_unit_c),   INK, TextAlign::CENTER_LEFT, "C");       // C


      // Humidity(%)
      if (isnan(id(s_rh).state)) snprintf(buf, sizeof(buf), "--");
      else                      snprintf(buf, sizeof(buf), "%.0f%%", id(s_rh).state);
      it.printf(CX3, CY, id(f_value_pct), INK, TextAlign::CENTER, "%s", buf);

Food level monitoring, environment tracking, and feeding automation.

YAML
esphome:
  name: seeed-feeder
  friendly_name: Seeed-Feeder
  platformio_options:
    board_build.arduino.memory_type: qio_opi
    board_build.flash_mode: qio
    board_build.psram_type: opi

esp32:
  board: esp32-s3-devkitc-1
  variant: esp32s3
  framework:
    type: arduino

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "tnqW2UDoBYDSqzGsDsMnNCjMXculp6oLSP7gMuxt4dg="

ota:
  - platform: esphome
    password: "cb70ec6e7bb71611efb6f17a78ef8a3b"

web_server:
  port: 80

wifi:
  ssid: "Maker_HA_2.4G"
  password: "maker2025"

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Seeed-Feeder Fallback Hotspot"
    password: "xosVifNqmxZ5"

captive_portal:

substitutions:
  ultra_sig_pin: "5"   # 单线超声波 SIG 所在 GPIO
  servo_sig_pin: "6"   # 伺服信号 GPIO

# --------- I2C(DHT20 用) ----------
i2c:
  - id: bus_a
    sda: GPIO5       # D4 / SDA(扩展板 Grove I2C)
    scl: GPIO6       # D5 / SCL(扩展板 Grove I2C)
    frequency: 100kHz
    scan: true

  - id: cam_bus
    sda: 40
    scl: 39
    scan: false

# --------- 传感器 ----------
sensor:
  - platform: aht10
    variant: AHT20
    address: 0x38
    i2c_id: bus_a
    update_interval: 10s
    temperature:
      name: Feeder Temp C
      id: feeder_temp_c
    humidity:
      name: Feeder RH
      id: feeder_humidity

# 单线超声波(把模块 SIG 接到 D2,供电接 5V/GND)
# 这里用 template + pulseIn,实现单线触发/回波
# 单线超声波(D0 = GPIO1)
  - platform: template
    name: "Feeder Distance cm"
    id: feed_distance
    unit_of_measurement: "cm"
    accuracy_decimals: 1
    update_interval: 2s
    lambda: |-
      const int SIG = 1;   // ← D0 对应 GPIO1,用数字 1
      pinMode(SIG, OUTPUT);
      digitalWrite(SIG, LOW);
      delayMicroseconds(2);
      digitalWrite(SIG, HIGH);
      delayMicroseconds(12);
      digitalWrite(SIG, LOW);
      pinMode(SIG, INPUT);
      unsigned long us = pulseIn(SIG, HIGH, 60000UL);
      ESP_LOGD("ultra", "pulse=%lu us", us);
      if (us == 0) return NAN;
      float cm = us / 58.0f;
      if (cm < 2.0f || cm > 400.0f) return NAN;
      return cm;

  # 余粮百分比(空/满线性映射)
  - platform: template
    name: "Feeder Fill Pct"
    id: feed_fill_pct
    unit_of_measurement: "%"
    accuracy_decimals: 0
    update_interval: 2s
    lambda: |-
      const float d_empty = 12.0;  // ← TODO: 标定空仓(cm)
      const float d_full  =  3.0;  // ← TODO: 标定满仓(cm)
      if (isnan(id(feed_distance).state)) return NAN;
      float d = id(feed_distance).state;
      float de = d_empty, df = d_full;
      if (de < df) { float t = de; de = df; df = t; }
      if (d < df) d = df;
      if (d > de) d = de;
      float pct = (de - d) / (de - df) * 100.0f;
      if (pct < 0) pct = 0;
      if (pct > 100) pct = 100;
      return pct;

# --------- 摄像头(XIAO ESP32S3 Sense) ----------
esp32_camera:
  name: "Feeder Camera"
  external_clock:
    pin: 10
    frequency: 20MHz
  i2c_id: cam_bus
  data_pins:
    - 15  # Y2
    - 17  # Y3
    - 18  # Y4
    - 16  # Y5
    - 14  # Y6
    - 12  # Y7
    - 11  # Y8
    - 48  # Y9
  vsync_pin: 38
  href_pin: 47
  pixel_clock_pin: 13
  resolution: 640x480
  jpeg_quality: 12
  max_framerate: 20 fps
  idle_framerate: 0 fps
  vertical_flip: false
  horizontal_mirror: false

# --------- 伺服(投喂) ----------
output:
  - platform: ledc
    pin: GPIO9       # 扩展板 D10,舵机信号线接这里
    id: servo_pwm
    frequency: 50 Hz

servo:
  - id: feeder_servo
    output: servo_pwm
    min_level: 5%      
    max_level: 10%     
    auto_detach_time: 0s
number:
  # 舵机开/关位置(0.0~1.0),按机构校准
  - platform: template
    name: "Servo Open Level"
    id: servo_open_level
    min_value: 0.0
    max_value: 1.0
    step: 0.02
    initial_value: 0.60
    optimistic: true

  - platform: template
    name: "Servo Close Level"
    id: servo_close_level
    min_value: 0.0
    max_value: 1.0
    step: 0.02
    initial_value: 0.00
    optimistic: true

  - platform: template
    name: "Pulse Open ms"
    id: pulse_open_ms
    min_value: 100
    max_value: 2000
    step: 50
    initial_value: 400
    unit_of_measurement: "ms"
    optimistic: true

  - platform: template
    name: "Pulse Pause ms"
    id: pulse_pause_ms
    min_value: 100
    max_value: 2000
    step: 50
    initial_value: 300
    unit_of_measurement: "ms"
    optimistic: true

  - platform: template
    name: "Pulses per Portion"
    id: pulses_per_portion
    min_value: 1
    max_value: 10
    step: 1
    initial_value: 5
    optimistic: true

  - platform: template
    name: "Portions to Feed"
    id: portions_to_feed
    min_value: 1
    max_value: 10
    step: 1
    initial_value: 1
    optimistic: true

  - platform: template
    name: "Portion Pause ms"
    id: portion_pause_ms
    min_value: 300
    max_value: 5000
    step: 100
    initial_value: 800
    unit_of_measurement: "ms"
    optimistic: true

button:
  - platform: template
    name: "Servo Test Sweep"
    on_press:
      - script.execute: sweep_servo
  - platform: template
    name: "Feed N Portions"
    on_press:
      - script.execute:
          id: feed_portions
          count: !lambda "return (int) id(portions_to_feed).state;"
  # === 急停按钮 ===
  - platform: template
    name: "Stop Feeding"
    on_press:
      - script.execute: stop_feeding

script:
  - id: sweep_servo
    then:
      - repeat:
          count: 3
          then:
            - servo.write: { id: feeder_servo, level: 0.0 }
            - delay: 600ms
            - servo.write: { id: feeder_servo, level: 1.0 }
            - delay: 600ms
  
  - id: feed_pulse
    mode: restart
    then:
      - servo.write:
          id: feeder_servo
          level: !lambda "return id(servo_open_level).state;"
      - delay: !lambda "return (uint32_t) id(pulse_open_ms).state;"
      - servo.write:
          id: feeder_servo
          level: !lambda "return id(servo_close_level).state;"
      - delay: !lambda "return (uint32_t) id(pulse_pause_ms).state;"

  # 一份 = N 个脉冲
  - id: feed_one_portion
    mode: queued
    then:
      - repeat:
          count: !lambda "return (int) id(pulses_per_portion).state;"
          then:
            - script.execute: feed_pulse
      - delay: !lambda "return (uint32_t) id(portion_pause_ms).state;"

  # 喂多份
  - id: feed_portions
    mode: queued
    parameters:
      count: int
    then:
      - repeat:
          count: !lambda "return count;"
          then:
            - script.execute: feed_one_portion

  - id: stop_feeding
    mode: restart
    then:
      # 终止所有可能在运行/排队中的喂食脚本
      - script.stop: feed_portions
      - script.stop: feed_one_portion
      - script.stop: feed_pulse
      # 舵机回到关闭位
      - servo.write:
          id: feeder_servo
          level: !lambda "return id(servo_close_level).state;"
      # 给机械一个短暂复位时间
      - delay: 300ms
      # 如需立刻释放力矩,可把 servo 的 auto_detach_time 设成 0s,

# --------- 诊断 ----------
binary_sensor:
  - platform: status
    name: "Feeder Online"

text_sensor:
  - platform: version
    name: "Feeder FW Version"

Credits

Liora Chen
1 project • 3 followers
Atom Yang
3 projects • 1 follower
A Product Manager.

Comments