// Sparrow Zigbee nodes: switch (end device) and light (router) #include #include #include "driver/gpio.h" #include "Zigbee.h" constexpr uint8_t SWITCH_PIN = 9; constexpr uint32_t FACTORY_RESET_HOLD_MS = 3000; constexpr uint32_t BUTTON_DEBOUNCE_MS = 50; #if defined(SPARROW_SWITCH) #include "ep/ZigbeeSwitch.h" #include "zcl/esp_zigbee_zcl_command.h" #include "zcl/esp_zigbee_zcl_on_off.h" constexpr uint8_t SWITCH_ENDPOINT = 1; constexpr uint32_t COMMISSIONING_AWAKE_MS = 15000; constexpr uint32_t POST_COMMAND_HOLD_MS = 250; constexpr uint32_t BUTTON_RELEASE_TIMEOUT_MS = 5000; ZigbeeSwitch switchEp(SWITCH_ENDPOINT); RTC_DATA_ATTR bool switchIsOn = false; static void waitForButtonRelease() { uint32_t start = millis(); while (digitalRead(SWITCH_PIN) == LOW) { uint32_t heldMs = millis() - start; if (heldMs >= FACTORY_RESET_HOLD_MS) { Serial.println("Factory reset Zigbee (switch)..."); Zigbee.factoryReset(true); // does not return } if (heldMs >= BUTTON_RELEASE_TIMEOUT_MS) { break; } delay(10); } } static void enterDeepSleep(uint32_t graceMs = 0) { if (graceMs > 0) { delay(graceMs); } Serial.println("Switch entering deep sleep…"); Serial.flush(); esp_deep_sleep_start(); } static bool sendOnOffCommand(bool turnOn) { esp_zb_zcl_on_off_cmd_t cmd_req = {}; cmd_req.address_mode = ESP_ZB_APS_ADDR_MODE_DST_ADDR_ENDP_NOT_PRESENT; cmd_req.zcl_basic_cmd.src_endpoint = SWITCH_ENDPOINT; cmd_req.on_off_cmd_id = turnOn ? ESP_ZB_ZCL_CMD_ON_OFF_ON_ID : ESP_ZB_ZCL_CMD_ON_OFF_OFF_ID; esp_zb_lock_acquire(portMAX_DELAY); esp_err_t err = esp_zb_zcl_on_off_cmd_req(&cmd_req); esp_zb_lock_release(); if (err != ESP_OK) { Serial.printf("Failed to send on/off command: 0x%x\n", err); return false; } return true; } void setup() { Serial.begin(115200); delay(20); pinMode(SWITCH_PIN, INPUT_PULLUP); esp_err_t pdCfg = esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); if (pdCfg != ESP_OK) { Serial.printf("RTC peripheral power config failed: 0x%x\n", pdCfg); } esp_err_t wakeCfg = esp_deep_sleep_enable_gpio_wakeup(1ULL << SWITCH_PIN, ESP_GPIO_WAKEUP_GPIO_LOW); if (wakeCfg != ESP_OK) { Serial.printf("Failed to enable EXT0 wakeup: 0x%x\n", wakeCfg); } switchEp.setManufacturerAndModel("Sparrow", "Switch"); switchEp.setPowerSource(ZB_POWER_SOURCE_BATTERY, 100); Zigbee.addEndpoint(&switchEp); Serial.println("Starting Zigbee (switch)..."); if (!Zigbee.begin()) { Serial.println("Zigbee failed to start, restarting…"); delay(1000); ESP.restart(); } Serial.print("Joining network"); uint32_t joinStart = millis(); while (!Zigbee.connected()) { Serial.print("."); delay(200); if (millis() - joinStart > 20000) { Serial.println("\nJoin timeout, restarting…"); ESP.restart(); } } Serial.println("\nJoined!"); esp_sleep_wakeup_cause_t wakeCause = esp_sleep_get_wakeup_cause(); bool wokeFromButton = (wakeCause == ESP_SLEEP_WAKEUP_EXT0); if (wokeFromButton) { switchIsOn = !switchIsOn; Serial.printf("Switch state → %s\n", switchIsOn ? "ON" : "OFF"); if (sendOnOffCommand(switchIsOn)) { Serial.println("On/Off command sent."); } waitForButtonRelease(); enterDeepSleep(POST_COMMAND_HOLD_MS); } else { Serial.println("Cold boot: staying awake for commissioning window."); delay(COMMISSIONING_AWAKE_MS); waitForButtonRelease(); enterDeepSleep(); } } void loop() { enterDeepSleep(); } #elif defined(SPARROW_LIGHT) #include #include "ep/ZigbeeLight.h" constexpr uint8_t LIGHT_ENDPOINT = 1; constexpr uint8_t NEOPIXEL_PIN = 3; constexpr uint8_t NEOPIXEL_COUNT = 1; constexpr uint8_t NEOPIXEL_BRIGHTNESS = 48; constexpr uint32_t IDENTIFY_BLINK_INTERVAL_MS = 250; constexpr uint32_t IDENTIFY_DEFAULT_DURATION_MS = 5000; ZigbeeLight lightEp(LIGHT_ENDPOINT); Adafruit_NeoPixel statusPixel(NEOPIXEL_COUNT, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800); static bool lightState = false; static bool lastButtonLevel = HIGH; static uint32_t lastButtonChangeMs = 0; static uint32_t pressStartMs = 0; static bool longPressHandled = false; static bool identifyActive = false; static bool identifyBlinkOn = false; static bool identifyReset = false; static uint32_t identifyDeadlineMs = 0; static uint32_t identifyLastToggleMs = 0; static void refreshNeoPixel() { if (identifyActive) { if (identifyBlinkOn) { statusPixel.setPixelColor(0, statusPixel.Color(0, 0, 255)); // blue during identify } else { statusPixel.clear(); } } else { if (lightState) { statusPixel.setPixelColor(0, statusPixel.Color(255, 200, 80)); // warm white } else { statusPixel.clear(); } } statusPixel.show(); } static void handleLightChange(bool on) { lightState = on; if (!identifyActive) { refreshNeoPixel(); } } static void handleIdentify(uint16_t seconds) { if (seconds == 0) { identifyActive = false; identifyReset = true; return; } uint32_t durationMs = seconds > 0 ? static_cast(seconds) * 1000UL : IDENTIFY_DEFAULT_DURATION_MS; if (durationMs == 0) { durationMs = IDENTIFY_DEFAULT_DURATION_MS; } identifyDeadlineMs = millis() + durationMs; identifyLastToggleMs = 0; identifyBlinkOn = false; identifyActive = true; identifyReset = true; } void setup() { Serial.begin(115200); pinMode(SWITCH_PIN, INPUT_PULLUP); statusPixel.begin(); statusPixel.setBrightness(NEOPIXEL_BRIGHTNESS); statusPixel.clear(); statusPixel.show(); lightEp.setManufacturerAndModel("Sparrow", "Light"); lightEp.onLightChange(handleLightChange); lightEp.onIdentify(handleIdentify); Zigbee.addEndpoint(&lightEp); Serial.println("Starting Zigbee (light)..."); if (!Zigbee.begin()) { Serial.println("Zigbee failed to start, restarting…"); delay(1000); ESP.restart(); } Serial.print("Joining network"); while (!Zigbee.connected()) { Serial.print("."); delay(200); } Serial.println("\nJoined!"); lightEp.setLight(false); } void loop() { uint32_t now = millis(); if (identifyReset) { identifyReset = false; refreshNeoPixel(); } if (identifyActive) { if ((int32_t)(identifyDeadlineMs - now) <= 0) { identifyActive = false; refreshNeoPixel(); } else if (identifyLastToggleMs == 0 || (now - identifyLastToggleMs) >= IDENTIFY_BLINK_INTERVAL_MS) { identifyLastToggleMs = now; identifyBlinkOn = !identifyBlinkOn; refreshNeoPixel(); } } bool reading = digitalRead(SWITCH_PIN); if (reading != lastButtonLevel && (now - lastButtonChangeMs) >= BUTTON_DEBOUNCE_MS) { lastButtonChangeMs = now; lastButtonLevel = reading; if (reading == LOW) { pressStartMs = now; longPressHandled = false; } else { if (pressStartMs != 0 && !longPressHandled) { bool newState = !lightState; Serial.printf("Local light toggle → %s\n", newState ? "ON" : "OFF"); lightEp.setLight(newState); } pressStartMs = 0; } } if (lastButtonLevel == LOW && pressStartMs != 0 && !longPressHandled && (now - pressStartMs) >= FACTORY_RESET_HOLD_MS) { longPressHandled = true; identifyActive = false; identifyReset = true; Serial.println("Factory reset Zigbee (light)..."); Zigbee.factoryReset(true); } delay(10); } #else #error "Define SPARROW_SWITCH or SPARROW_LIGHT for this firmware." #endif