// Test sketch for the ESP32-WROOM-32 DevKit board (30 pins)
// of the “humidificador dormitorio” project. Controlled via Homey (WiFi) and
// the Serial Monitor.
//
// GPIO2 = opto channel 1 (dry contact, simulates touching the humidifier’s
// original capacitive ON/mode button).
// GPIO18 = trigger for the relay module (Songle SRD-05VDC-SL-C) that cuts and
// restores the humidifier’s 24V supply.
//
// Business logic (fixed 2026-07-19 to go back to what the original brief
// intended): the relay and the opto are NOT independent, they run as a
// single sequence triggered by the “onoff” capability:
// - OFF: the relay is cut briefly and RESTORES itself (non-blocking
// sequence) → the humidifier ends up idle but still WITH power,
// same as if someone had unplugged and replugged it.
// This way the humidifier’s physical/capacitive button keeps working
// after an OFF from Homey (if the relay were left cut
// permanently, the humidifier would be left without power and the
// manual button wouldn’t respond). The opto is never touched on OFF.
// - ON: the relay is NOT touched (the humidifier should already be idle
// after the last OFF). It just waits a short settling margin
// and then activates the opto to simulate touching the
// ON/mode button. If ON arrives while the relay is still
// cut (e.g. ON right after an OFF), it’s restored first.
//
// Both modules can be wired to trigger active-high or active-low depending on
// jumper/design → if either one activates backwards from what’s expected (clicks or
// lights its LED when it shouldn’t), flip the corresponding _ACTIVE_LOW.
#include <WiFi.h>
#include <WiFiClient.h>
#include <ArduinoOTA.h>
#include <Homey.h>
// — Network configuration —
const char* WIFI_SSID = “MOVISTAR_4A08”;
const char* WIFI_PASSWORD = “TU_CONTRASEÑA_WIFI”;
const int RELAY_PIN = 18;
const bool RELAY_ACTIVE_LOW = false; // confirmed 2026-07-18
const int OPTO1_PIN = 2;
const bool OPTO1_ACTIVE_LOW = false; // adjust if the simulated tap doesn’t register
const unsigned long RELAY_PULSE_MS = 500; // duration of the cut during shutdown, then restores itself
const unsigned long OPTO_SETTLE_MS = 300; // wait before tapping the button (in case the relay just restored)
const unsigned long OPTO_PULSE_MS = 250; // duration of the simulated tap on the opto
// OPTO_SETTLE_MS and OPTO_PULSE_MS are a starting point, they need tuning
// by testing whether the humidifier actually turns on with these timings.
bool humidifierOn = false; // logical state reported to Homey
void setRelayCut(bool cut) {
bool pinLevel = RELAY_ACTIVE_LOW ? !cut : cut;
digitalWrite(RELAY_PIN, pinLevel ? HIGH : LOW);
}
bool relayIsCut = false; // reflects the last call to setRelayCut()
void setRelayCutTracked(bool cut) {
relayIsCut = cut;
setRelayCut(cut);
}
void setOptoTap(bool active) {
bool pinLevel = OPTO1_ACTIVE_LOW ? !active : active;
digitalWrite(OPTO1_PIN, pinLevel ? HIGH : LOW);
}
// Non-blocking sequences (no delay() in loop()):
// OFF: cut relay → wait RELAY_PULSE_MS → restore relay → idle
// ON: (if the relay was still cut, restore it now) → wait
// OPTO_SETTLE_MS → tap opto → wait OPTO_PULSE_MS → release opto
enum ActionStage { STAGE_IDLE, STAGE_OFF_CUTTING, STAGE_ON_SETTLING, STAGE_ON_TAPPING };
ActionStage stage = STAGE_IDLE;
unsigned long stageStartMillis = 0;
void startTurnOffSequence() {
stage = STAGE_OFF_CUTTING;
stageStartMillis = millis();
setRelayCutTracked(true);
Serial.println(“OFF: cutting power for 0.5s (restores itself)…”);
}
void startTurnOnSequence() {
if (relayIsCut) {
setRelayCutTracked(false);
}
stage = STAGE_ON_SETTLING;
stageStartMillis = millis();
Serial.println(“ON: waiting to settle before tapping the button…”);
}
void updateActionSequence() {
if (stage == STAGE_IDLE) return;
unsigned long elapsed = millis() - stageStartMillis;
if (stage == STAGE_OFF_CUTTING && elapsed >= RELAY_PULSE_MS) {
setRelayCutTracked(false);
stage = STAGE_IDLE;
Serial.println(“OFF: power restored, humidifier idle.”);
} else if (stage == STAGE_ON_SETTLING && elapsed >= OPTO_SETTLE_MS) {
setOptoTap(true);
stageStartMillis = millis();
stage = STAGE_ON_TAPPING;
Serial.println(“ON: tapping button (opto)…”);
} else if (stage == STAGE_ON_TAPPING && elapsed >= OPTO_PULSE_MS) {
setOptoTap(false);
stage = STAGE_IDLE;
Serial.println(“ON: sequence complete.”);
}
}
void turnOff() {
setOptoTap(false); // cancels any opto tap in progress
humidifierOn = false;
// Confirms the value to Homey; without this Homey doesn’t know the command
// was applied and after several unconfirmed attempts ends up marking
// the device as “not found”.
Homey.setCapabilityValue(“onoff”, humidifierOn);
startTurnOffSequence();
}
void turnOn() {
humidifierOn = true;
Homey.setCapabilityValue(“onoff”, humidifierOn);
startTurnOnSequence();
}
// Callback called by Homey when the “onoff” switch is pressed in the app
void onHomeyOnOff() {
bool wantOn = Homey.value.toInt(); // toInt() to correctly convert “1”/“0” to bool
if (wantOn) {
turnOn();
} else {
turnOff();
}
}
void processSerialCommand(String command) {
command.trim();
command.toLowerCase();
if (command == “on”) {
turnOn();
} else if (command == “off”) {
turnOff();
} else if (command == “rele on”) {
setRelayCutTracked(true);
Serial.println(“Relay: manual cut (no sequence)”);
} else if (command == “rele off”) {
setRelayCutTracked(false);
Serial.println(“Relay: manual restore (no sequence)”);
} else if (command == “opto on”) {
setOptoTap(true);
Serial.println(“Opto: manually activated”);
} else if (command == “opto off”) {
setOptoTap(false);
Serial.println(“Opto: manually deactivated”);
} else if (command.length() > 0) {
Serial.println(“Unrecognized command. Use: on / off / rele on / rele off / opto on / opto off”);
}
}
void setup() {
Serial.begin(115200);
delay(500); // gives the native USB Serial time before printing
pinMode(RELAY_PIN, OUTPUT);
setRelayCutTracked(false); // starts without touching the relay = humidifier keeps whatever power it already had
pinMode(OPTO1_PIN, OUTPUT);
setOptoTap(false);
Serial.println();
Serial.println(“Board started. ESP32-WROOM-32 DevKit (30 pins).”);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print(“Connecting to WiFi”);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(“.”);
}
Serial.println();
Serial.print("Connected. IP: ");
Serial.println(WiFi.localIP());
ArduinoOTA.setHostname(“HumidificadorDormitorio”);
ArduinoOTA.setPassword(“0000”); // the IDE requires something in the Password field, doesn’t accept empty
ArduinoOTA.onStart( {
Serial.println(“OTA: update started.”);
});
ArduinoOTA.onEnd( {
Serial.println(“OTA: update complete, restarting.”);
});
ArduinoOTA.onError((ota_error_t error) {
Serial.printf(“OTA: error [%u]\n”, error);
});
ArduinoOTA.begin();
Serial.println(“OTA ready (password 0000). In Arduino IDE: Tools > Port > HumidificadorDormitorio over network.”);
Homey.begin(“HumidificadorDormitorio”);
Homey.setClass(“light”);
Homey.addCapability(“onoff”, onHomeyOnOff);
Serial.println(“Ready (OTA test #1). Use the onoff switch in Homey, or over Serial: on / off / rele on / rele off / opto on / opto off”);
}
void loop() {
ArduinoOTA.handle(); // must be called constantly, no delay() in this loop
Homey.loop(); // must be called constantly, no delay() in this loop
updateActionSequence();
if (Serial.available() > 0) {
String command = Serial.readStringUntil(‘\n’);
processSerialCommand(command);
}
}