Created
November 29, 2025 21:47
-
-
Save ChrisRomp/3a1342e580dc5c56c6c0b315b85ec2b7 to your computer and use it in GitHub Desktop.
ESP8266 Kitty Counter Defender
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| esphome: | |
| name: counter-defender-1 | |
| friendly_name: Counter Defender 1 | |
| on_boot: | |
| priority: -10 | |
| then: | |
| - text_sensor.template.publish: | |
| id: system_status | |
| state: "Disarmed" | |
| esp8266: | |
| # Using nodemcuv2 is safer for pin mapping (D1, D2 etc), | |
| # but since you requested esp01_1m, we use raw GPIO numbers. | |
| board: esp01_1m | |
| # Enable logging | |
| logger: | |
| # Enable Home Assistant API | |
| api: | |
| encryption: | |
| key: "" | |
| ota: | |
| - platform: esphome | |
| password: "" | |
| wifi: | |
| ssid: !secret wifi_ssid | |
| password: !secret wifi_password | |
| ap: | |
| ssid: "Counter-Defender-1" | |
| password: "" | |
| captive_portal: | |
| # --- GLOBAL VARIABLES --- | |
| globals: | |
| # Tracks if the system is fully armed and ready to trigger | |
| - id: system_ready | |
| type: bool | |
| restore_value: no | |
| initial_value: 'false' | |
| # --- HARDWARE DEFINITIONS --- | |
| text_sensor: | |
| - platform: template | |
| name: "System Status" | |
| id: system_status | |
| icon: "mdi:shield-check-outline" | |
| output: | |
| # Status LED (Onboard NodeMCU LED is GPIO2, Active LOW) | |
| - platform: gpio | |
| pin: | |
| number: 2 | |
| inverted: true | |
| id: status_led_output | |
| binary_sensor: | |
| # Physical Button (Arm/Disarm) - GPIO14 (D5) | |
| - platform: gpio | |
| pin: | |
| number: 14 | |
| mode: INPUT_PULLUP | |
| inverted: true | |
| name: "Physical Button" | |
| filters: | |
| - delayed_on: 50ms # Debounce | |
| on_press: | |
| - switch.toggle: arm_switch | |
| # PIR Motion Sensor - GPIO4 (D2) | |
| - platform: gpio | |
| pin: 4 | |
| name: "PIR Sensor" | |
| device_class: motion | |
| on_press: | |
| then: | |
| # Only trigger if system is fully ready (post-countdown) AND not currently executing a trigger | |
| - if: | |
| condition: | |
| and: | |
| - lambda: 'return id(system_ready);' | |
| - not: | |
| script.is_running: trigger_sequence | |
| then: | |
| - script.execute: trigger_sequence | |
| # --- LOGIC & AUTOMATION --- | |
| switch: | |
| # Main Switch exposed to Home Assistant | |
| - platform: template | |
| name: "Arm System" | |
| id: arm_switch | |
| optimistic: true | |
| turn_on_action: | |
| - script.execute: arming_sequence | |
| turn_off_action: | |
| # Cancel all active scripts and reset state | |
| - script.stop: arming_sequence | |
| - script.stop: trigger_sequence | |
| - globals.set: | |
| id: system_ready | |
| value: 'false' | |
| - output.turn_off: status_led_output | |
| - text_sensor.template.publish: | |
| id: system_status | |
| state: "Disarmed" | |
| - logger.log: "System Disarmed" | |
| script: | |
| # 1. ARMING SEQUENCE (30s Delay with Blinking) | |
| - id: arming_sequence | |
| then: | |
| - text_sensor.template.publish: | |
| id: system_status | |
| state: "Arming" | |
| - logger.log: "Arming initiated... 30s countdown" | |
| # Loop 60 times (60 * 500ms = 30s) | |
| - repeat: | |
| count: 60 | |
| then: | |
| - output.turn_on: status_led_output | |
| - delay: 250ms | |
| - output.turn_off: status_led_output | |
| - delay: 250ms | |
| # Finalize Arming | |
| - globals.set: | |
| id: system_ready | |
| value: 'true' | |
| - output.turn_on: status_led_output # Solid ON means Armed | |
| - text_sensor.template.publish: | |
| id: system_status | |
| state: "Armed" | |
| - logger.log: "System Fully ARMED" | |
| # 2. TRIGGER SEQUENCE (Siren + 5s Cooldown) | |
| - id: trigger_sequence | |
| then: | |
| - text_sensor.template.publish: | |
| id: system_status | |
| state: "Alarm" | |
| - logger.log: "Motion Detected! Firing Siren." | |
| # Fire Event to Home Assistant | |
| - homeassistant.event: | |
| event: esphome.counter_defender_triggered | |
| # Visual indicator (LED OFF briefly) | |
| - output.turn_off: status_led_output | |
| # WARBLING SIREN (C++ Lambda for direct tone control) | |
| # GPIO12 is D6 | |
| - lambda: |- | |
| int pin = 12; | |
| unsigned long startTime = millis(); | |
| bool sweepUp = true; | |
| int currentFreq = 8000; | |
| while(millis() - startTime < 1000) { | |
| if (sweepUp) { | |
| currentFreq += 50; | |
| if (currentFreq >= 12000) sweepUp = false; | |
| } else { | |
| currentFreq -= 50; | |
| if (currentFreq <= 8000) sweepUp = true; | |
| } | |
| tone(pin, currentFreq, 1); | |
| delay(1); | |
| } | |
| noTone(pin); | |
| # COOLDOWN (Blink fast for 5s) | |
| - logger.log: "Cooling down..." | |
| - repeat: | |
| count: 50 # 50 * 100ms = 5s | |
| then: | |
| - output.turn_on: status_led_output | |
| - delay: 50ms | |
| - output.turn_off: status_led_output | |
| - delay: 50ms | |
| # Return to Ready State | |
| - output.turn_on: status_led_output | |
| - text_sensor.template.publish: | |
| id: system_status | |
| state: "Armed" | |
| - logger.log: "Ready for next trigger." |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * NodeMCU Cat Deterrent System | |
| * * Hardware: | |
| * - NodeMCU ESP8266 | |
| * - Mini PIR Sensor (Uxcell a15032000ux0258) | |
| * - Passive Buzzer | |
| * - Tactile Push Button | |
| * * Logic: | |
| * - Button toggles system ARM/DISARM | |
| * - 30-second arming delay allows user to leave area | |
| * - Motion triggers 1-second warbling siren (8-12kHz) | |
| * - 5-second cool-down period prevents rapid re-triggering | |
| * - Status LED blinks during arming/cooldown and is solid when ARMED | |
| */ | |
| // --- Pin Definitions --- | |
| const int PIR_PIN = D2; // GPIO4: PIR Sensor Input | |
| const int BUTTON_PIN = D5; // GPIO14: Arm/Disarm Button Input | |
| const int BUZZER_PIN = D6; // GPIO12: Passive Buzzer Output (Avoids boot conflict pins) | |
| const int STATUS_LED_PIN = D4; // GPIO2: Onboard Status LED (Active LOW) | |
| // --- System State Variables --- | |
| enum State { DISARMED, ARMING_DELAY, ARMED, COOLING_DOWN }; | |
| State systemState = DISARMED; | |
| unsigned long armingStartTime = 0; | |
| unsigned long cooldownStartTime = 0; | |
| const unsigned long ARMING_DELAY_MS = 30000; // 30 seconds to leave the room | |
| const unsigned long RESET_DELAY_MS = 5000; // 5 seconds software cool-down after trigger | |
| int lastButtonState = HIGH; // For button debouncing/state tracking | |
| // --- Buzzer Tone Parameters --- | |
| const int TONE_FREQ_START = 8000; // Start frequency (8 kHz) | |
| const int TONE_FREQ_END = 12000; // End frequency (12 kHz) | |
| const unsigned long SIREN_DURATION_MS = 1000; // 1 second warble duration | |
| void setup() { | |
| Serial.begin(19200); | |
| Serial.println("Starting up..."); | |
| Serial.println("PIR Sensor Warming up... DO NOT MOVE for 10 seconds."); | |
| // Initial hardware stabilization delay | |
| delay(10000); | |
| // Initialize pins | |
| pinMode(PIR_PIN, INPUT); | |
| pinMode(BUZZER_PIN, OUTPUT); | |
| pinMode(STATUS_LED_PIN, OUTPUT); | |
| pinMode(BUTTON_PIN, INPUT_PULLUP); | |
| // Initial State: Disarmed (LED OFF) | |
| setSystemState(DISARMED); | |
| Serial.println("System Ready."); | |
| } | |
| void loop() { | |
| // 1. Handle Button Press to Toggle State (Non-blocking and always active) | |
| handleButtonToggle(); | |
| // 2. State Logic (Non-blocking) | |
| switch (systemState) { | |
| case DISARMED: | |
| // Nothing to do but wait for button press | |
| break; | |
| case ARMING_DELAY: | |
| // Blink LED during countdown (250ms interval) | |
| blinkStatusLED(250); | |
| // Check if the 30-second delay has expired | |
| if (millis() - armingStartTime >= ARMING_DELAY_MS) { | |
| setSystemState(ARMED); | |
| } | |
| break; | |
| case ARMED: | |
| // Ensure LED is solid ON (Active LOW) | |
| digitalWrite(STATUS_LED_PIN, LOW); | |
| // Check for motion only if fully ARMED | |
| if (digitalRead(PIR_PIN) == HIGH) { | |
| Serial.println("!!! Motion Detected - Triggering Deterrent !!!"); | |
| triggerDeterrent(); // This is the only BLOCKING part (1 second siren) | |
| // Immediately transition to COOLING_DOWN state | |
| setSystemState(COOLING_DOWN); | |
| } | |
| break; | |
| case COOLING_DOWN: | |
| // Blink LED rapidly during cooldown (100ms interval) | |
| blinkStatusLED(100); | |
| // Check if the 5-second cooldown has expired | |
| if (millis() - cooldownStartTime >= RESET_DELAY_MS) { | |
| // Return to active ARMED state | |
| setSystemState(ARMED); | |
| } | |
| break; | |
| } | |
| // Brief delay to keep the loop responsive | |
| delay(10); | |
| } | |
| // --- Helper Functions --- | |
| // Toggles the system state between DISARMED and ARMING_DELAY | |
| void handleButtonToggle() { | |
| int currentButtonState = digitalRead(BUTTON_PIN); | |
| // Look for press release (active low button logic) | |
| if (currentButtonState == LOW && lastButtonState == HIGH) { | |
| if (systemState == DISARMED) { | |
| setSystemState(ARMING_DELAY); | |
| } else { | |
| // Disarm immediately from ANY state (ARMING_DELAY, ARMED, or COOLING_DOWN) | |
| setSystemState(DISARMED); | |
| } | |
| delay(50); // Simple debounce | |
| } | |
| lastButtonState = currentButtonState; | |
| } | |
| // Sets the system state and updates initial LED/Serial status | |
| void setSystemState(State newState) { | |
| systemState = newState; | |
| switch (systemState) { | |
| case DISARMED: | |
| digitalWrite(STATUS_LED_PIN, HIGH); // LED OFF (Active HIGH) | |
| Serial.println("System DISARMED."); | |
| break; | |
| case ARMING_DELAY: | |
| armingStartTime = millis(); // Record the start time | |
| Serial.println("System ARMING. Waiting for stabilization/delay..."); | |
| break; | |
| case ARMED: | |
| digitalWrite(STATUS_LED_PIN, LOW); // LED ON (Active LOW) | |
| Serial.println("System ARMED and Active."); | |
| break; | |
| case COOLING_DOWN: | |
| cooldownStartTime = millis(); // Record the start time | |
| Serial.println("Triggered! Cooling down..."); | |
| break; | |
| } | |
| } | |
| // Blinks the LED based on the interval (Non-blocking) | |
| void blinkStatusLED(unsigned long interval) { | |
| static unsigned long lastBlinkTime = 0; | |
| if (millis() - lastBlinkTime >= interval) { | |
| lastBlinkTime = millis(); | |
| // Toggle the LED state | |
| int ledState = digitalRead(STATUS_LED_PIN); | |
| digitalWrite(STATUS_LED_PIN, !ledState); | |
| } | |
| } | |
| // Trigger the deterrent (buzzer) with a warbling siren | |
| void triggerDeterrent() { | |
| Serial.println("Starting 1-second Warbling Siren..."); | |
| // Temporarily turn LED OFF during siren start | |
| digitalWrite(STATUS_LED_PIN, HIGH); | |
| unsigned long sirenStartTime = millis(); | |
| bool sweepUp = true; | |
| int currentFreq = TONE_FREQ_START; | |
| // This loop is blocking for 1 second to ensure the full siren plays | |
| while (millis() - sirenStartTime < SIREN_DURATION_MS) { | |
| // --- Non-Blocking Frequency Sweep Logic --- | |
| if (sweepUp) { | |
| currentFreq += 50; | |
| if (currentFreq >= TONE_FREQ_END) { | |
| sweepUp = false; | |
| } | |
| } else { | |
| currentFreq -= 50; | |
| if (currentFreq <= TONE_FREQ_START) { | |
| sweepUp = true; | |
| } | |
| } | |
| // Play the new frequency for a very short duration (1 ms) | |
| tone(BUZZER_PIN, currentFreq, 1); | |
| delay(1); | |
| } | |
| // Stop the tone immediately | |
| noTone(BUZZER_PIN); | |
| Serial.println("Siren complete."); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment