diff --git a/README.md b/README.md index 003d62d..acd4a33 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This project transforms a standard Hunter sprinkler/irrigation controller (with It exposes each sprinkler zone as an individual Zigbee switch, enabling you to control your irrigation system using automations, dashboards, and voice assistants. The firmware is designed to be mostly stateless, treating Home Assistant as the single source of truth for all schedules and timers. ## Features -- Individual Zone Control: Exposes up to 4 sprinkler (can be easily modified to support more zones) zones as separate Zigbee endpoints. +- Individual Zone Control: Exposes up to 8 sprinkler zones (configurable from 1-48) as separate Zigbee endpoints. - Stateless Operation: Designed to be controlled by Home Assistant automations; the device itself holds no schedules. @@ -69,7 +69,7 @@ graph TD ``` ## Setup and Installation -- Configure the Code: Open main.cpp and adjust any configuration constants at the top of the file if needed (e.g., NUM_ZONES). The default signal pin (D5) is for the XIAO ESP32-C6. +- Configure the Code: Open main.cpp and adjust any configuration constants at the top of the file if needed. The firmware is configured for 8 zones by default. To change the number of zones, modify NUM_ZONES (supports 1-48 zones). The default signal pin (D5) is for the XIAO ESP32-C6. - Compile and Upload: Using PlatformIO or the Arduino IDE, compile and upload the firmware to your ESP32. This project used board: XIAO ESP32-C6. @@ -81,7 +81,7 @@ graph TD - Power on the ESP32. The onboard LED should start blinking. - - Home Assistant should discover the device and its 4 zones. Serial console logs should look healthy. The device would log switch changes from HA. + - Home Assistant should discover the device and its 8 zones. Serial console logs should look healthy. The device would log switch changes from HA. - Final Installation: Once paired & verified, disconnect the ESP32, wire it to the Hunter controller's REM port as per the diagram, and power it back on. diff --git a/build/bootloader.bin b/build/bootloader.bin new file mode 100644 index 0000000..f3bd828 Binary files /dev/null and b/build/bootloader.bin differ diff --git a/build/firmware.bin b/build/firmware.bin new file mode 100644 index 0000000..03c58bb Binary files /dev/null and b/build/firmware.bin differ diff --git a/build/partitions.bin b/build/partitions.bin new file mode 100644 index 0000000..8e5c8e6 Binary files /dev/null and b/build/partitions.bin differ diff --git a/lib/HunterRoam/HunterRoam.cpp b/lib/HunterRoam/HunterRoam.cpp index f264d26..bdb046e 100644 --- a/lib/HunterRoam/HunterRoam.cpp +++ b/lib/HunterRoam/HunterRoam.cpp @@ -22,8 +22,8 @@ * Thanks to all the previous authors/works. * * --- Komal-SkyNET (Komal Venkatesh Ganesan) --- Sept 2025 - * - * / + * + */ #include "HunterRoam.h" diff --git a/src/main.cpp b/src/main.cpp index eb4da41..9873efd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,23 +8,29 @@ #define LED_PIN LED_BUILTIN #define LED_ON LOW // For many boards, the built-in LED is active-low (LOW turns it on) #define LED_OFF HIGH -#define NUM_ZONES 4 -#define SAFETY_TIMEOUT_MINUTES 60 // Safety shut-off time in minutes -#define WDT_TIMEOUT_SECONDS 30 // Watchdog Timer: reboot if the main loop freezes for this long. Increased for stability. - -// The ZoneConfig is simplified. Home Assistant will manage names and all timing. -// We only need to define the Zigbee endpoint and a model name for identification. -struct ZoneConfig { - const char* modelName; - uint8_t endpoint; -}; +#define NUM_ZONES 8 // Number of irrigation zones (accepts 1-48 zones). +#define SAFETY_TIMEOUT_MINUTES 60 // Safety shut-off time in minutes +#define WDT_TIMEOUT_SECONDS 30 // Watchdog Timer: reboot if the main loop freezes for this long. Increased for stability. + +#if NUM_ZONES < 1 + #error "NUM_ZONES must be at least 1" +#endif +#if NUM_ZONES > 48 + #error "NUM_ZONES cannot exceed 48 (Hunter controller limit)" +#endif +/***********************************************/ + +// Helper function to generate zone name dynamically +String getZoneName(uint8_t index) { + return "Zone " + String(index + 1); +} -ZoneConfig zones[NUM_ZONES] = { - {"Zone 1", 10}, - {"Zone 2", 11}, - {"Zone 3", 12}, - {"Zone 4", 13} -}; +// Helper function to get Zigbee endpoint for a zone +// Note: Endpoint IDs start at 10 (not to be confused with zone numbers which start at 1) +// Zone 1 = zigbeeEndpoint 10, Zone 2 = zigbeeEndpoint 11, etc. +uint8_t getZoneEndpoint(uint8_t index) { + return 10 + index; // index is 0-based, so Zone 1 (index 0) gets endpoint 10 +} /********************* Hardware Instances *********************/ HunterRoam hunter(SMARTPORT_PIN); @@ -46,7 +52,7 @@ void handleZoneChange(uint8_t index, bool requestedState) { uint8_t zoneNumber = index + 1; // The HunterRoam library is 1-based if (requestedState) { - Serial.printf("Received ON request for zone %d (%s) with %d-minute safety timer\n", zoneNumber, zones[index].modelName, SAFETY_TIMEOUT_MINUTES); + Serial.printf("Received ON request for zone %d (%s) with %d-minute safety timer\n", zoneNumber, getZoneName(index).c_str(), SAFETY_TIMEOUT_MINUTES); byte err = hunter.startZone(zoneNumber, SAFETY_TIMEOUT_MINUTES); if (err != 0) { @@ -58,7 +64,7 @@ void handleZoneChange(uint8_t index, bool requestedState) { zoneSafetyOffTime[index] = millis() + (SAFETY_TIMEOUT_MINUTES * 60 * 1000UL); } } else { - Serial.printf("Received OFF request for zone %d (%s)\n", zoneNumber, zones[index].modelName); + Serial.printf("Received OFF request for zone %d (%s)\n", zoneNumber, getZoneName(index).c_str()); byte err = hunter.stopZone(zoneNumber); if (err != 0) { @@ -76,11 +82,66 @@ void handleZoneChange(uint8_t index, bool requestedState) { #define MAKE_ZONE_CALLBACK(N) \ void onZone##N(bool state){ handleZoneChange(N, state); } -// Generate onZone0, onZone1, onZone2, onZone3 +// Generate callbacks for all 48 possible zones (Hunter controller limit) +// Only NUM_ZONES callbacks will be used and attached in setup() MAKE_ZONE_CALLBACK(0) MAKE_ZONE_CALLBACK(1) MAKE_ZONE_CALLBACK(2) MAKE_ZONE_CALLBACK(3) +MAKE_ZONE_CALLBACK(4) +MAKE_ZONE_CALLBACK(5) +MAKE_ZONE_CALLBACK(6) +MAKE_ZONE_CALLBACK(7) +MAKE_ZONE_CALLBACK(8) +MAKE_ZONE_CALLBACK(9) +MAKE_ZONE_CALLBACK(10) +MAKE_ZONE_CALLBACK(11) +MAKE_ZONE_CALLBACK(12) +MAKE_ZONE_CALLBACK(13) +MAKE_ZONE_CALLBACK(14) +MAKE_ZONE_CALLBACK(15) +MAKE_ZONE_CALLBACK(16) +MAKE_ZONE_CALLBACK(17) +MAKE_ZONE_CALLBACK(18) +MAKE_ZONE_CALLBACK(19) +MAKE_ZONE_CALLBACK(20) +MAKE_ZONE_CALLBACK(21) +MAKE_ZONE_CALLBACK(22) +MAKE_ZONE_CALLBACK(23) +MAKE_ZONE_CALLBACK(24) +MAKE_ZONE_CALLBACK(25) +MAKE_ZONE_CALLBACK(26) +MAKE_ZONE_CALLBACK(27) +MAKE_ZONE_CALLBACK(28) +MAKE_ZONE_CALLBACK(29) +MAKE_ZONE_CALLBACK(30) +MAKE_ZONE_CALLBACK(31) +MAKE_ZONE_CALLBACK(32) +MAKE_ZONE_CALLBACK(33) +MAKE_ZONE_CALLBACK(34) +MAKE_ZONE_CALLBACK(35) +MAKE_ZONE_CALLBACK(36) +MAKE_ZONE_CALLBACK(37) +MAKE_ZONE_CALLBACK(38) +MAKE_ZONE_CALLBACK(39) +MAKE_ZONE_CALLBACK(40) +MAKE_ZONE_CALLBACK(41) +MAKE_ZONE_CALLBACK(42) +MAKE_ZONE_CALLBACK(43) +MAKE_ZONE_CALLBACK(44) +MAKE_ZONE_CALLBACK(45) +MAKE_ZONE_CALLBACK(46) +MAKE_ZONE_CALLBACK(47) + +// Array of function pointers for dynamic callback attachment +void (*zoneCallbacks[48])(bool) = { + onZone0, onZone1, onZone2, onZone3, onZone4, onZone5, onZone6, onZone7, + onZone8, onZone9, onZone10, onZone11, onZone12, onZone13, onZone14, onZone15, + onZone16, onZone17, onZone18, onZone19, onZone20, onZone21, onZone22, onZone23, + onZone24, onZone25, onZone26, onZone27, onZone28, onZone29, onZone30, onZone31, + onZone32, onZone33, onZone34, onZone35, onZone36, onZone37, onZone38, onZone39, + onZone40, onZone41, onZone42, onZone43, onZone44, onZone45, onZone46, onZone47 +}; /********************* Helper Functions for Main Loop *********/ @@ -210,16 +271,15 @@ void setup() { // Create and register Zigbee endpoints for each valve for (uint8_t i = 0; i < NUM_ZONES; i++) { - valves[i] = new ZigbeeLight(zones[i].endpoint); + valves[i] = new ZigbeeLight(getZoneEndpoint(i)); valves[i]->setManufacturerAndModel("SkynetIrrigation", "Controller"); Zigbee.addEndpoint(valves[i]); } - // Attach callbacks - valves[0]->onLightChange(onZone0); - valves[1]->onLightChange(onZone1); - valves[2]->onLightChange(onZone2); - valves[3]->onLightChange(onZone3); + // Attach callbacks dynamically based on NUM_ZONES + for (uint8_t i = 0; i < NUM_ZONES; i++) { + valves[i]->onLightChange(zoneCallbacks[i]); + } // Temporarily remove our task from the watchdog before starting Zigbee, // as Zigbee.begin() can block for a long time if the hub is not nearby.