335 lines
9.5 KiB
YAML
335 lines
9.5 KiB
YAML
# =============================================================================
|
|
# 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;' |