# ============================================================================= # LITTER BOX MONITOR - DFRobot ESP32-S3 AI Camera # ============================================================================= substitutions: device_name: litter-box-cam friendly_name: "Litter Box Camera" # ----------------------------------------------------------------------------- # Core Device Configuration # ----------------------------------------------------------------------------- esphome: name: ${device_name} friendly_name: ${friendly_name} platformio_options: build_flags: "-DBOARD_HAS_PSRAM" board_build.arduino.memory_type: qio_opi esp32: board: esp32-s3-devkitc-1 framework: type: arduino version: latest flash_size: 16MB # ----------------------------------------------------------------------------- # PSRAM Configuration (Critical for camera) # ----------------------------------------------------------------------------- psram: mode: octal speed: 80MHz # ----------------------------------------------------------------------------- # Logging & API # ----------------------------------------------------------------------------- logger: level: INFO api: encryption: key: !secret api_encryption_key services: - service: capture_and_analyze then: - script.execute: capture_image_script - service: update_status variables: needs_scooping: bool litter_level_low: bool cat_present: bool cleanliness_score: int then: - lambda: |- id(needs_scooping_sensor).publish_state(needs_scooping); id(litter_level_low_sensor).publish_state(litter_level_low); id(cat_present_sensor).publish_state(cat_present); id(cleanliness_score_sensor).publish_state(cleanliness_score); id(last_analysis_time).publish_state(id(homeassistant_time).now().timestamp); if (cat_present) { id(daily_cat_visits) += 1; id(cat_visits_sensor).publish_state(id(daily_cat_visits)); } ota: - platform: esphome password: !secret ota_password # ----------------------------------------------------------------------------- # WiFi Configuration # ----------------------------------------------------------------------------- wifi: ssid: !secret wifi_iot_ssid password: !secret wifi_password ap: ssid: "${device_name}-AP" password: !secret wifi_password captive_portal: # ----------------------------------------------------------------------------- # MQTT Configuration # ----------------------------------------------------------------------------- mqtt: id: mqtt_client broker: !secret mqtt_broker username: !secret mqtt_username password: !secret mqtt_password topic_prefix: litter_box # ----------------------------------------------------------------------------- # Time Synchronization # ----------------------------------------------------------------------------- time: - platform: homeassistant id: homeassistant_time on_time: - seconds: 0 minutes: 0 hours: 0 then: - lambda: |- id(daily_cat_visits) = 0; id(cat_visits_sensor).publish_state(0); # ----------------------------------------------------------------------------- # Global Variables # ----------------------------------------------------------------------------- globals: - id: daily_cat_visits type: int restore_value: yes initial_value: '0' - id: motion_cooldown_active type: bool initial_value: 'false' - id: publish_next_image type: bool initial_value: 'false' # ----------------------------------------------------------------------------- # Camera Configuration (OV3660 - DFR1154 Specific) # ----------------------------------------------------------------------------- esp32_camera: id: litter_cam name: "Litter Box Camera" # Hardware Interface - DFR1154 Pinout external_clock: pin: GPIO5 frequency: 20MHz i2c_pins: sda: GPIO8 scl: GPIO9 data_pins: [GPIO16, GPIO18, GPIO21, GPIO17, GPIO14, GPIO7, GPIO6, GPIO4] vsync_pin: GPIO1 href_pin: GPIO2 pixel_clock_pin: GPIO15 # Image Parameters max_framerate: 10 fps idle_framerate: 0.1 fps resolution: 800x600 jpeg_quality: 10 vertical_flip: true horizontal_mirror: false aec_mode: AUTO agc_mode: AUTO wb_mode: AUTO brightness: 0 contrast: 1 saturation: 0 on_image: then: - lambda: |- if (!id(publish_next_image)) return; id(publish_next_image) = false; const uint8_t *data = x->get_data(); size_t len = x->get_data_length(); size_t out_len = 4 * ((len + 2) / 3); char *buf = (char *)malloc(out_len + 1); if (!buf) return; static const char b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; size_t j = 0; for (size_t i = 0; i < len; i += 3) { uint32_t b = ((uint32_t)data[i] << 16); if (i + 1 < len) b |= ((uint32_t)data[i + 1] << 8); if (i + 2 < len) b |= data[i + 2]; buf[j++] = b64[(b >> 18) & 0x3F]; buf[j++] = b64[(b >> 12) & 0x3F]; buf[j++] = (i + 1 < len) ? b64[(b >> 6) & 0x3F] : '='; buf[j++] = (i + 2 < len) ? b64[b & 0x3F] : '='; } buf[j] = '\0'; id(mqtt_client).publish("litter_box/camera/image", buf); ESP_LOGI("capture", "Published image (%d bytes raw, %d base64)", len, j); id(status_message).publish_state("Image sent for analysis"); free(buf); # Web server for manual viewing esp32_camera_web_server: - port: 8080 mode: stream - port: 8081 mode: snapshot # ----------------------------------------------------------------------------- # GPIO Outputs # ----------------------------------------------------------------------------- output: - id: led_status platform: gpio pin: GPIO3 - id: ir_led platform: gpio pin: GPIO47 - id: speaker_gain platform: gpio pin: GPIO41 - id: speaker_mode platform: gpio pin: GPIO40 # ----------------------------------------------------------------------------- # Lights # ----------------------------------------------------------------------------- light: - platform: binary name: "Status LED" id: status_led output: led_status - platform: binary name: "IR Night Vision" id: ir_night_vision output: ir_led restore_mode: RESTORE_DEFAULT_OFF # ----------------------------------------------------------------------------- # Binary Sensors # ----------------------------------------------------------------------------- binary_sensor: - platform: template name: "Motion Detected" id: motion_detected device_class: motion - platform: template name: "Needs Scooping" id: needs_scooping_sensor device_class: problem icon: "mdi:shovel" - platform: template name: "Litter Level Low" id: litter_level_low_sensor device_class: problem icon: "mdi:cup-outline" - platform: template name: "Cat Present" id: cat_present_sensor device_class: occupancy icon: "mdi:cat" # ----------------------------------------------------------------------------- # Sensors # ----------------------------------------------------------------------------- sensor: - platform: wifi_signal name: "WiFi Signal" update_interval: 60s - platform: template name: "Cleanliness Score" id: cleanliness_score_sensor unit_of_measurement: "%" icon: "mdi:star" accuracy_decimals: 0 - platform: template name: "Daily Cat Visits" id: cat_visits_sensor icon: "mdi:paw" accuracy_decimals: 0 - platform: template name: "Last Analysis" id: last_analysis_time device_class: timestamp icon: "mdi:clock-outline" # ----------------------------------------------------------------------------- # Text Sensors # ----------------------------------------------------------------------------- text_sensor: - platform: version name: "ESPHome Version" - platform: template name: "Status Message" id: status_message icon: "mdi:message-text" # ----------------------------------------------------------------------------- # Switches # ----------------------------------------------------------------------------- switch: - platform: restart name: "Restart Camera" - platform: template name: "Auto Capture Enabled" id: auto_capture_enabled optimistic: true restore_mode: RESTORE_DEFAULT_ON # ----------------------------------------------------------------------------- # Buttons # ----------------------------------------------------------------------------- button: - platform: template name: "Capture Now" icon: "mdi:camera" on_press: - script.execute: capture_image_script # ----------------------------------------------------------------------------- # Scripts # ----------------------------------------------------------------------------- script: - id: capture_image_script mode: single then: - lambda: 'ESP_LOGI("capture", "Capturing image...");' - light.turn_on: status_led - delay: 200ms - lambda: |- id(publish_next_image) = true; id(litter_cam).request_image(esphome::esp32_camera::SINGLE_SHOT); - light.turn_off: status_led - lambda: 'id(motion_cooldown_active) = true;' - delay: 30s - lambda: 'id(motion_cooldown_active) = false;'