/* For Sonoff POW and Sonoff TH devices Startup function setup() : read device config from flash memory (top 64k) connect to WiFi and MQTT Server in main programm loop(): verify WiFi & MQTT connection, try to recover and reboot if not working poll sensors and publish values every 10 seconds do householding tasks if a MQTT message arrives on topic command/'device' : verify payload, publish warning to input/'device' if corrupt act on payload command: 1 relay: set relay and publish confirmation 2 config: retrieve config and save config in SPIFFS, publish confirmation 3 sw update: fetch new firmware using HTTP and restart 4 restart: restart in a timer function every 50ms read button status and switch relay if pressed longer than 1 sec In addition it offers: - Firmware over the air update FOTA, secured by key - save relay state in EEPROM to set the same state after power up or restart Compile Options for Arduino IDE: CPU Frequency: 160MHz, Flash Size: 1M (64k SPIFFS), Board: Generic ESP8266 */ #include // needed for the WiFi communication #include // for FOTA Suport #include // ditto #include // ditto #include // interrupt based scheduler #include // to save RELAY state accross power outage #include // to parse and safe the values in the config file #include "FS.h" // file system to safe the config in the upper 64k of memory #include // MQTT Client from Joël Gaehwiler https://github.com/256dpi/arduino-mqtt #include // for sonoff POW measurement routines #include "DHT.h" // for sonoff TH10 which uses the temperature sensor DHT22 #include // for Firmware Update using HTTP #include // ditto const char* Version = "{\"Version\":\"Sonoff_Generic_v1k1\"}"; const char* msg_measure_fail = "{\"Status\":\"measurement failed, retry in 10s.\"}"; unsigned long lastMeasurement = 0; // counter for sensor measurements unsigned long now; // will contain actual time unsigned long pressCount = 0; // contains the time the button is pressed bool publishRelay = false; // tells main loop to publish relay status uint32_t ip_sensor; // for the static IP address of the device uint32_t ip_gateway; // for the static IP address of the router/gateway uint32_t ip_subnet; // for the subnet mask 255.255.255.0 String input_topic; // input/'device' for data from device to broker String cmd_topic; // command/'device' for data from broker to device String payload_str; // used to compose message to the broker uint8_t RELAY = 12; // for Sonoff POW and Sonoff TH : Relay and red LED uint8_t LED = 15; // for Sonoff POW and Sonoff TH : blue LED uint8_t BUTTON = 0; // for Sonoff POW and Sonoff TH : button String DType; // contains the device type (sonoffpow, sonoffth, test) unsigned long MsgNumber = 0; bool WiFiRestart; String HostnameWifi; const char* Hostname; const char* MQTT_User; const char* MQTT_PW; const char* MQTT_Broker; const char* WiFi_SSID; const char* WiFi_PW; WiFiClientSecure TCP; // TCP client object, uses SSL/TLS MQTTClient mqttClient(512); // MQTT client object with a buffer size of 512 (needed for config file) ESP8266PowerClass powRead; // power measurement object Ticker buttonTimer; // object to read button status DHT dht(14, 22); // DHT22 Type Sensor on Pin 14 for Sonoff TH devices boolean parseIP(uint32_t* addr, const char* str) { // Converts IP address string to 4 byte uint8_t *part = (uint8_t*)addr; // found on the net, no clue how this works byte i; *addr = 0; for (i = 0; i < 4; i++) { part[i] = strtoul(str, NULL, 10); // convert one byte str = strchr(str, '.'); if (str == NULL || *str == '\0') break; // no more separators, exit str++; // point to next character after separator } return (i == 3); // true if there were 4 values separated by dots } // end IP address conversion void command_callback(String &cctopic, String &ccpayload) { // Callback is called when MQTT messages is received StaticJsonBuffer<700> commandBuffer; JsonObject& root = commandBuffer.parseObject(ccpayload); const char* Command = root["Command"]; // get command number from JSON string int command_nr = String(Command).toInt(); if (command_nr == 1) { // command: set relay const char* Relay1 = root["Relay1"]; if (strncmp(Relay1, "on", 2) == 0) { // check command and act digitalWrite(RELAY, HIGH); // turn relay on publishRelay = true; // do the MQTT publish in main loop } else { digitalWrite(RELAY, LOW); // turn relay off publishRelay = true; // do the MQTT publish in main loop } } else if (command_nr == 2) { // command: load new configfunction File wconfigFile = SPIFFS.open("/config.json", "w"); // write to config file in SPIFFS root.printTo(wconfigFile); mqttClient.publish(input_topic, "{\"Status\":\"config updated\"}"); } else if (command_nr == 3) { // command: load new firmware buttonTimer.detach(); // detach Ticker routine for button handling, not needed anymore const char* FW = root["FW"]; String FW_URL = "http://192.168.11.71/sw/" + String(FW); mqttClient.publish(input_topic, "{\"Status\":\"loading new firmware: "+FW_URL+"\"}"); mqttClient.loop(); delay(1000); // wait to get all debug info out of the door mqttClient.disconnect(); // gracefully disconnect delay(1000); // wait to get it done ESPhttpUpdate.update(FW_URL); // and now update the firmware and restart } else if (command_nr == 4) { // command: restart mqttClient.publish(input_topic, "{\"Status\":\"Received reset command: restarting...\"}"); delay(1000); ESP.restart(); } } // end MQTT callback function void read_button() { // start check if button is pressed, if (!digitalRead(BUTTON)) { // ( called 20 times per second by Ticker ) pressCount++; } else { if (pressCount > 20) { // button is at least 1 second pressed digitalWrite(RELAY, !digitalRead(RELAY)); // immediately switch the relay publishRelay = true; // do the MQTT publish in main loop due to timing } pressCount = 0; } } // end void setup() { SPIFFS.begin(); File rconfigFile = SPIFFS.open("/config.json", "r"); // Start read config from SPIFFS size_t sizefile = rconfigFile.size(); std::unique_ptr buf(new char[sizefile]); rconfigFile.readBytes(buf.get(), sizefile); StaticJsonBuffer<700> jsonBuffer; JsonObject& readc = jsonBuffer.parseObject(buf.get()); // parse the JSON string from the config file Hostname = readc["Hostname"]; // and save parameters in variables for later use MQTT_User = readc["MQTT_User"]; MQTT_PW = readc["MQTT_PW"]; WiFi_SSID = readc["WiFi_SSID"]; WiFi_PW = readc["WiFi_PW"]; const char* Sensor_IP = readc["Sensor_IP"]; const char* Gateway_IP = readc["Gateway_IP"]; const char* Subnet_IP = readc["Subnet_IP"]; MQTT_Broker = readc["MQTT_Broker"]; const char* Device_Type = readc["Device_Type"]; const char* AES_Key = readc["AES_Key"]; parseIP(&ip_sensor, Sensor_IP); // convert the IP address strings to 4 Byte form parseIP(&ip_gateway, Gateway_IP); parseIP(&ip_subnet, Subnet_IP); DType = String(Device_Type); if (DType == "sonoffth") { // start sonoff TH specific config RELAY = 12; LED = 15; BUTTON = 0; dht.begin(); // initialize DHT sensor }else if (DType == "sonoffpow") { // start sonoff POW specific config RELAY = 12; LED = 15; BUTTON = 0; powRead.enableMeasurePower(); // initialize power measurement powRead.selectMeasureCurrentOrVoltage(CURRENT); powRead.startMeasure(); } else { // start ESP8266 dev board config RELAY = 16; LED = 14; } pinMode(LED, OUTPUT); // start setup GPIOs for relay and LED digitalWrite(LED, HIGH); // blue LED, turn it on pinMode(RELAY, OUTPUT); // set GPIO pin for relay/red LED EEPROM.begin(8); // start use of EEPROM, where the old relay state is stored if (EEPROM.read(0)) {digitalWrite(RELAY, HIGH);} // end and switch relay to on if it was on before power loss WiFi.mode(WIFI_STA); // start setup WiFi WiFi.config(ip_sensor, ip_gateway, ip_subnet); // set fix WiFi config delay(10); WiFi.begin(WiFi_SSID, WiFi_PW); delay(100); for ( int i = 0; i < 300; i++) { // try to connect to WiFi for max 30s if (WiFi.status() == WL_CONNECTED) {break;} delay(100); } if (WiFi.status() != WL_CONNECTED) { // WiFi failed delay(5000); // Wait 5 sec before reboot ESP.restart(); } HostnameWifi = Hostname; HostnameWifi.concat(".local"); WiFi.hostname(HostnameWifi); // end at this point WiFi is set up MDNS.begin(Hostname); // start OTA. First start MDNS ArduinoOTA.setHostname(Hostname); // initialize and start OTA ArduinoOTA.setPassword(AES_Key); // the AES key is also the OTA password ArduinoOTA.onError([](ota_error_t error) {ESP.restart();}); // restart in case of an error ArduinoOTA.onStart([]() {buttonTimer.detach();}); // stop button interrup routine while new SW loads ArduinoOTA.begin(); delay(100); // end at this point OTA is set up mqttClient.begin(MQTT_Broker, 8883, TCP); // start config MQTT Server mqttClient.onMessage(command_callback); // call this routine of message is posted for the device input_topic = "input/" + String(Hostname); cmd_topic = "command/" + String(Hostname); // end MQTT config, the connection is done in the main loop buttonTimer.attach(0.05, read_button); // reads button status 20 times per second } void loop() { if (WiFi.status() != WL_CONNECTED) { // Check for WiFi and try to recover if needed WiFi.begin(WiFi_SSID, WiFi_PW); for ( int i = 0; i < 300; i++) { // try to connect to WiFi for max 30s if (WiFi.status() == WL_CONNECTED) {break;} delay(100); } } // at this point WiFi should be set up if (WiFi.status() != WL_CONNECTED) { // if WiFi still failed, then delay(5000); // wait 5 sec and reboot ESP.restart(); } if (!mqttClient.connected() || WiFiRestart) { // start MQTT connection if not connected for ( int i = 0; i < 30; i++) { // try to connect to MQTT for max 30s if (mqttClient.connect(Hostname, MQTT_User, MQTT_PW)) {break;} delay(1000); } if (mqttClient.connected()) { // if MQTT connection was successful, then mqttClient.subscribe(cmd_topic); // subscribe to commands via broker from node-red mqttClient.publish(input_topic, Version); // publish SW version as a (re-)connect info mqttClient.loop(); delay(100); // and get the messages out of the door before continuing WiFiRestart = false; } else { // else MQTT failed, wait 5 sec and reboot delay(5000); ESP.restart(); } // at this point WiFi and MQTT are up and running } now = millis(); // Start measuring every 10 sec if (now - lastMeasurement > 10000) { mqttClient.loop(); delay(10); // (hint from Joël, seems to fix some problems) lastMeasurement = now; MsgNumber++; // this is the message number for the current measurement run payload_str = "{\"MsgNr\":"; // create a JSON formatted string for sensor data payload_str += MsgNumber; if (DType == "sonoffth") { float t = dht.readTemperature() -1.0; // read temperature from sensor and correct typical DHT22 offset float h = dht.readHumidity(); // read humidity from sensor if (isnan(h) || isnan(t)) { mqttClient.publish(input_topic, msg_measure_fail); } else { payload_str += ",\"Temp\":" + String(t); payload_str += ",\"Hum\":" + String(h) + "}"; // at this stage the JSON sensor data object is finished mqttClient.publish(input_topic, payload_str); // publish finished measurement } delay(100); // get broker message out of the door } if (DType == "sonoffpow") { double power = powRead.getPower(); yield(); double current = powRead.getCurrent(); yield(); if (isnan(power) || isnan(current)) { mqttClient.publish(input_topic, msg_measure_fail); } else { payload_str += ",\"Power\":" + String(power); payload_str += ",\"Current\":" + String(current) + "}"; // at this stage the JSON sensor data object is finished mqttClient.publish(input_topic, payload_str); // publish finished measurement } delay(100); // get broker message out of the door } if (DType =="test") { // it's just the test device without any sensors payload_str += "}"; // we publish at least the message number as status mqttClient.publish(input_topic, payload_str); // so we know it is alive and well delay(100); // get broker message out of the door } } // end measuring section if (publishRelay) { // start publish relay state and save to EEPROM MsgNumber++; // this is the new message number payload_str = "{\"MsgNr\":"; // create a JSON formatted string for relay status payload_str += MsgNumber; if (digitalRead(RELAY)) { // figure out the status of the relay payload_str += ",\"Relay1\":\"on\"}"; EEPROM.write(0,1); } else { payload_str += ",\"Relay1\":\"off\"}"; EEPROM.write(0,0); } EEPROM.commit(); // save relay state to EEPROM mqttClient.publish(input_topic, payload_str); // send relay state to Broker delay(100); // get broker message out of the door publishRelay = false; // end publish relay state, reset flag } ArduinoOTA.handle(); // start section with regular household functions mqttClient.loop(); delay(10); } // end