├── README.md └── esp-mqtt-btn-switch └── esp-mqtt-btn-switch.ino /README.md: -------------------------------------------------------------------------------- 1 | # ESP8266 Momentary Standalone Relay Switch with MQTT 2 | 3 | This is used to toggle a relay with a push button, using the EX-Store [WiFi-Relay Board](https://ex-store.de/ESP8266-WiFi-Relay-V31). The Switch will work if no WiFi or MQTT is present. I'm using it in combination with a coupling relay to replace a surge switch (Eltako S12-100). 4 | 5 | See also the excelent [Espurna Firmware](https://bitbucket.org/xoseperez/espurna) which supports the ex-store relay board. 6 | 7 | It should work on any ESP8266 with a connected relay. The difference will be, that the EX-Store relay board is using a [L9110](http://www.elecrow.com/download/datasheet-l9110.pdf) motor driver which needs two GPIO's to be switched. To change this behaviour simply edit the `turnOn()` or `turnOff()` functions. 8 | 9 | Connect the push button to `GND` and `IO14` to toggle the relay. The internal Pull-Up is used. 10 | 11 | After first flashing this sketch, you could update the ESP via [ArduinoOTA](http://esp8266.github.io/Arduino/versions/2.3.0/doc/ota_updates/readme.html). The OTA name is set to the `mqttClienName` which is also used for the ESP-hostname. After an OTA update the ESP gets restarted. 12 | 13 | Starting with `0.0.3` you can attach a DHT temperature/humidity sensor to `IO04` (configurable). Please comment in the `USE_DHT` define to use this feature. 14 | 15 | ## MQTT 16 | * The current state (`1`/`0`) of the relay is published to `state`. 17 | * The online status (`online`/`offline`) is published to `status`. 18 | * The IP of the device is published to `ip`. 19 | 20 | These topics are retained and the status will be set to `offline` using the last will of MQTT. The mqtt connection is now non-blocking (finally) 21 | 22 | To change the switch' state, publish `1` or `0` to `do`. 23 | 24 | The temperature and humidity values (if used) will be read every 60 seconds (configurable), retained and put to this topics: 25 | 26 | * `temperature` 27 | * `humidity` 28 | 29 | 30 | ## Config 31 | Change the settings at the top of the .ino-file corresponding to your needs. Ensure that you choose a unique mqtt client id. If you do not need credentials just set user and password to an empty string. 32 | 33 | The serial console is left open to debug and check the WiFi connection. Baudrate 115200, 8N1. 34 | 35 | ## Problems 36 | If you get `error: 'class ArduinoOTAClass' has no member named 'getCommand'` comment out the complete `ArduinoOTA.onStart` method. It only puts the OTA-Type to the serial console when OTA is happening, so not realy needed. 37 | 38 | Also you can update `ArduinoOTA.cpp` and `ArduinoOTA.h` to the latest master from https://github.com/esp8266/Arduino/tree/master/libraries/ArduinoOTA 39 | 40 | 41 | ## Created with 42 | - Arduino 1.8.1 (https://www.arduino.cc/) 43 | - ESP8266 board definition 2.3.0 (https://github.com/esp8266/Arduino) 44 | - PubSubClient 2.6.0 by Nick O'Leary (https://github.com/knolleary/pubsubclient) 45 | - Arduino OTA 2.3.0 http://esp8266.github.io/Arduino/versions/2.3.0/doc/ota_updates/readme.html 46 | - Adafruit Unified Sensor library https://github.com/adafruit/Adafruit_DHT_Unified 47 | - Adafruit DHT library https://github.com/adafruit/DHT-sensor-library 48 | 49 | ## Misc. 50 | Bear with me as C/C++ is not my first language. Any suggestions and pull requests are welcome. 51 | -------------------------------------------------------------------------------- /esp-mqtt-btn-switch/esp-mqtt-btn-switch.ino: -------------------------------------------------------------------------------- 1 | // ESP8266 Momentary Standalone Relay Switch with MQTT 2 | // 2017, Patrik Mayer - patrik.mayer@codm.de 3 | // 4 | // debounce from http://blog.erikthe.red/2015/08/02/esp8266-button-debounce/ 5 | // mqtt client from https://github.com/knolleary/pubsubclient/tree/master/examples/mqtt_esp8266 6 | // Arduino OTA from https://github.com/esp8266/Arduino/tree/master/libraries/ArduinoOTA 7 | // DHT from https://github.com/adafruit/Adafruit_DHT_Unified 8 | 9 | // system state on status (retained) 10 | // system ip on ip (retained) 11 | // current switch state on state 12 | // send 1/0 to do to switch (retained) 13 | 14 | // if using DHT: 15 | // temperature on temperate (retained) 16 | // humidity on humidity (retained) 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | //install Adafruits's DHT_Unified and DHT 25 | #include 26 | #include 27 | #include 28 | 29 | //--------- Configuration 30 | // WiFi 31 | const char* ssid = ""; 32 | const char* password = ""; 33 | 34 | const char* mqttServer = ""; 35 | const char* mqttUser = ""; 36 | const char* mqttPass = ""; 37 | const char* mqttClientName = ""; //will also be used hostname and OTA name 38 | const char* mqttTopicPrefix = ""; 39 | 40 | 41 | //#define USE_DHT //comment in if DHT should be used 42 | #define DHTTYPE DHT22 //DHT11, DHT21, DHT22 43 | 44 | // I/O 45 | const int btnPin = 14; //IO14 on WiFi Relay 46 | 47 | #ifdef USE_DHT 48 | const int dhtPin = 5; //IO04 on WiFi Relay 49 | const int dhtInterval = 60000; //millis 50 | //--------- 51 | 52 | char mqttTopicTemp[64]; 53 | char mqttTopicHum[64]; 54 | 55 | long lastDHTTime = 0; 56 | #endif 57 | 58 | // internal vars 59 | WiFiClient espClient; 60 | PubSubClient client(espClient); 61 | 62 | char mqttTopicState[64]; 63 | char mqttTopicStatus[64]; 64 | char mqttTopicDo[64]; 65 | char mqttTopicIp[64]; 66 | 67 | long lastReconnectAttempt = 0; //For the non blocking mqtt reconnect (in millis) 68 | long lastDebounceTime = 0; // Holds the last time debounce was evaluated (in millis). 69 | const int debounceDelay = 80; // The delay threshold for debounce checking. 70 | 71 | int onoff = false; //is relay on or off 72 | int wantedState = false; //wanted state 73 | int debounceState; //internal state for debouncing 74 | 75 | #ifdef USE_DHT 76 | DHT_Unified dht(dhtPin, DHTTYPE); 77 | #endif 78 | 79 | void setup() { 80 | // Configure the pin mode as an input. 81 | pinMode(btnPin, INPUT_PULLUP); 82 | 83 | //output pins for L9110 84 | pinMode(12, OUTPUT); 85 | pinMode(13, OUTPUT); 86 | 87 | //switch off relay 88 | digitalWrite(12, HIGH); 89 | digitalWrite(13, LOW); 90 | 91 | Serial.begin(115200); 92 | 93 | // Attach an interrupt to the pin, assign the onChange function as a handler and trigger on changes (LOW or HIGH). 94 | attachInterrupt(btnPin, onChangeButton, CHANGE); 95 | 96 | #ifdef USE_DHT 97 | //initialize DHT 98 | dht.begin(); 99 | #endif 100 | 101 | //put in mqtt prefix 102 | sprintf(mqttTopicState, "%sstate", mqttTopicPrefix); 103 | sprintf(mqttTopicStatus, "%sstatus", mqttTopicPrefix); 104 | sprintf(mqttTopicDo, "%sdo", mqttTopicPrefix); 105 | sprintf(mqttTopicIp, "%sip", mqttTopicPrefix); 106 | 107 | #ifdef USE_DHT 108 | sprintf(mqttTopicTemp, "%stemperature", mqttTopicPrefix); 109 | sprintf(mqttTopicHum, "%shumidity", mqttTopicPrefix); 110 | #endif 111 | 112 | setup_wifi(); 113 | client.setServer(mqttServer, 1883); 114 | client.setCallback(mqttCallback); 115 | 116 | 117 | 118 | //----------- OTA 119 | ArduinoOTA.setHostname(mqttClientName); 120 | 121 | ArduinoOTA.onStart([]() { 122 | String type; 123 | if (ArduinoOTA.getCommand() == U_FLASH) { 124 | type = "sketch"; 125 | } else { // U_SPIFFS 126 | type = "filesystem"; 127 | } 128 | // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() 129 | Serial.println("Start updating " + type); 130 | }); 131 | 132 | ArduinoOTA.onEnd([]() { 133 | Serial.println("\nEnd"); 134 | delay(1000); 135 | ESP.restart(); 136 | }); 137 | 138 | ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { 139 | Serial.printf("Progress: %u%%\r", (progress / (total / 100))); 140 | }); 141 | 142 | ArduinoOTA.onError([](ota_error_t error) { 143 | Serial.printf("Error[%u]: ", error); 144 | if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); 145 | else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); 146 | else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); 147 | else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); 148 | else if (error == OTA_END_ERROR) Serial.println("End Failed"); 149 | }); 150 | ArduinoOTA.begin(); 151 | 152 | Serial.println("ready..."); 153 | } 154 | 155 | void loop() { 156 | 157 | //handle mqtt connection, non-blocking 158 | if (!client.connected()) { 159 | long now = millis(); 160 | if (now - lastReconnectAttempt > 5000) { 161 | lastReconnectAttempt = now; 162 | // Attempt to reconnect 163 | if (MqttReconnect()) { 164 | lastReconnectAttempt = 0; 165 | } 166 | } 167 | } 168 | client.loop(); 169 | 170 | //handle state change 171 | if (onoff != wantedState) { 172 | doOnOff(); 173 | } 174 | 175 | #ifdef USE_DHT 176 | //check DHT 177 | checkDHT(); 178 | #endif 179 | 180 | //handle OTA 181 | ArduinoOTA.handle(); 182 | 183 | } 184 | 185 | 186 | 187 | // Gets called by the interrupt. 188 | void onChangeButton() { 189 | 190 | int reading = digitalRead(btnPin); // Get the pin reading. 191 | if (reading == debounceState) return; // Ignore dupe readings. 192 | 193 | boolean debounce = false; 194 | 195 | // Check to see if the change is within a debounce delay threshold. 196 | if ((millis() - lastDebounceTime) <= debounceDelay) { 197 | debounce = true; 198 | } 199 | 200 | // This update to the last debounce check is necessary regardless of debounce state. 201 | lastDebounceTime = millis(); 202 | 203 | if (debounce) return; // Ignore reads within a debounce delay threshold. 204 | debounceState = reading; // All is good, persist the reading as the state. 205 | 206 | if (reading) { 207 | wantedState = !wantedState; 208 | } 209 | 210 | } 211 | 212 | void doOnOff() { 213 | onoff = !onoff; 214 | Serial.println("new relay state: " + String(onoff)); 215 | 216 | if (onoff) { 217 | turnOn(); 218 | } else { 219 | turnOff(); 220 | } 221 | } 222 | 223 | 224 | void turnOn() { 225 | //12 0, 13 1 226 | digitalWrite(12, LOW); 227 | digitalWrite(13, HIGH); 228 | 229 | client.publish(mqttTopicState, "1", true); 230 | 231 | } 232 | 233 | void turnOff() { 234 | //12 1, 13 0 235 | digitalWrite(12, HIGH); 236 | digitalWrite(13, LOW); 237 | 238 | client.publish(mqttTopicState, "0", true); 239 | } 240 | 241 | #ifdef USE_DHT 242 | void checkDHT() { 243 | 244 | if (millis() - lastDHTTime > dhtInterval) { 245 | 246 | sensors_event_t event; 247 | char dhtBuf[8]; 248 | 249 | dht.temperature().getEvent(&event); 250 | if (isnan(event.temperature)) { 251 | Serial.println("Error reading temperature!"); 252 | } else { 253 | sprintf(dhtBuf, "%.2f", event.temperature); 254 | client.publish(mqttTopicTemp, dhtBuf, true); 255 | 256 | Serial.print("Temperature: "); 257 | Serial.print(dhtBuf); 258 | Serial.println(" °C"); 259 | 260 | } 261 | 262 | // Get humidity event and print its value. 263 | dht.humidity().getEvent(&event); 264 | if (isnan(event.relative_humidity)) { 265 | Serial.println("Error reading humidity!"); 266 | } else { 267 | sprintf(dhtBuf, "%.2f", event.temperature); 268 | client.publish(mqttTopicHum, dhtBuf, true); 269 | 270 | Serial.print("Humidity: "); 271 | Serial.print(dhtBuf); 272 | Serial.println("%"); 273 | } 274 | 275 | lastDHTTime = millis(); 276 | } 277 | 278 | } 279 | #endif 280 | 281 | void setup_wifi() { 282 | 283 | delay(10); 284 | 285 | Serial.println(); 286 | Serial.print("Connecting to "); 287 | Serial.println(ssid); 288 | 289 | WiFi.mode(WIFI_STA); //disable AP mode, only station 290 | WiFi.hostname(mqttClientName); 291 | WiFi.begin(ssid, password); 292 | 293 | while (WiFi.status() != WL_CONNECTED) { 294 | delay(500); 295 | Serial.print("."); 296 | if (onoff != wantedState) { 297 | doOnOff(); 298 | } 299 | } 300 | 301 | Serial.println(""); 302 | Serial.println("WiFi connected"); 303 | Serial.println("IP address: "); 304 | Serial.println(WiFi.localIP()); 305 | } 306 | 307 | 308 | bool MqttReconnect() { 309 | 310 | if (!client.connected()) { 311 | 312 | Serial.print("Attempting MQTT connection..."); 313 | 314 | // Attempt to connect with last will retained 315 | if (client.connect(mqttClientName, mqttUser, mqttPass, mqttTopicStatus, 1, true, "offline")) { 316 | 317 | Serial.println("connected"); 318 | 319 | // Once connected, publish an announcement... 320 | char curIp[16]; 321 | sprintf(curIp, "%d.%d.%d.%d", WiFi.localIP()[0], WiFi.localIP()[1], WiFi.localIP()[2], WiFi.localIP()[3]); 322 | 323 | client.publish(mqttTopicStatus, "online", true); 324 | client.publish(mqttTopicState, ((onoff) ? "1" : "0") , true); 325 | client.publish(mqttTopicIp, curIp, true); 326 | 327 | // ... and (re)subscribe 328 | client.subscribe(mqttTopicDo); 329 | Serial.print("subscribed to "); 330 | Serial.println(mqttTopicDo); 331 | 332 | } else { 333 | Serial.print("failed, rc="); 334 | Serial.print(client.state()); 335 | Serial.println(" try again in 5 seconds"); 336 | } 337 | } 338 | return client.connected(); 339 | } 340 | 341 | 342 | void mqttCallback(char* topic, byte* payload, unsigned int length) { 343 | 344 | Serial.print("Message arrived ["); 345 | Serial.print(topic); 346 | Serial.print("] "); 347 | for (int i = 0; i < length; i++) { 348 | Serial.print((char)payload[i]); 349 | } 350 | Serial.println(); 351 | 352 | if ((char)payload[0] == '1') { 353 | wantedState = true; 354 | } else { 355 | wantedState = false; 356 | } 357 | 358 | } 359 | 360 | 361 | --------------------------------------------------------------------------------