diff --git a/esphome/litter-box-cam.yaml b/esphome/litter-box-cam.yaml new file mode 100644 index 0000000..c72d15c --- /dev/null +++ b/esphome/litter-box-cam.yaml @@ -0,0 +1,318 @@ +# ============================================================================= +# 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: + 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' + +# ----------------------------------------------------------------------------- +# 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 + +# 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: |- + auto cam = id(litter_cam); + camera::Camera::FrameBuffer *fb = cam->get_image(); + if (fb) { + size_t out_len = 4 * ((fb->length + 2) / 3); + char *base64_buffer = (char *)malloc(out_len + 1); + if (base64_buffer) { + size_t encoded_len = 0; + int ret = mbedtls_base64_encode( + (unsigned char *)base64_buffer, out_len + 1, &encoded_len, + fb->buf, fb->length); + if (ret == 0) { + base64_buffer[encoded_len] = '\0'; + id(mqtt_client).publish("litter_box/camera/image", base64_buffer); + ESP_LOGI("capture", "Published image"); + id(status_message).publish_state("Image sent for analysis"); + } + free(base64_buffer); + } + cam->return_image(fb); + } + - light.turn_off: status_led + - lambda: 'id(motion_cooldown_active) = true;' + - delay: 30s + - lambda: 'id(motion_cooldown_active) = false;' \ No newline at end of file