Skip to content

Instantly share code, notes, and snippets.

@ChrisRomp
Created November 29, 2025 21:47
Show Gist options
  • Select an option

  • Save ChrisRomp/3a1342e580dc5c56c6c0b315b85ec2b7 to your computer and use it in GitHub Desktop.

Select an option

Save ChrisRomp/3a1342e580dc5c56c6c0b315b85ec2b7 to your computer and use it in GitHub Desktop.
ESP8266 Kitty Counter Defender
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."
/*
* 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