The whole code is below.
Some of the configuration is in SPIFFS, stored as a JSON formatted string. But from the variable names it should be obvious what I fetch from there and how it is used.
And thanks for the link. I had a look at it. The scheduler looks interesting. You use the well known and proven MQTT library from knolleary. It does not support TLS encryption which I need in my environment. Else I would have just taken your code and be done with it.
As for error logs. I do not have any. The sonoff devices run on 220V mains, it is not adviseable to have them connected via the serial interface to the USB plug of my laptop. Which leaves me without any logging. I tried the Wifi terminal interface using the development phase, but deleted it from the operational code since it has no password protection.
The MQTT broker log (mosquitto on a Raspberry Pi) shows that the device does not send any keepalive messages and disconnects the device. So there is no useful information neither.
The core problem is the unreliable WiFi I have to cope with. From what I see the main loop of the code is still running fine. The code from the ticker routine which checks the button and switches the relay still works fine. But the whole WiFi and MQTT connection is broken, no communication with the device using WiFi is possible. But the tests at the beginning of the main loop do not detect the WiFi failure and therefore do not execute the recovery code.
Any help is very much appreciated.
Kind regards,
Urs.
/* 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 <ESP8266WiFi.h> // needed for the WiFi communication
#include <ESP8266mDNS.h> // for FOTA Suport
#include <WiFiUdp.h> // ditto
#include <ArduinoOTA.h> // ditto
#include <Ticker.h> // interrupt based scheduler
#include <EEPROM.h> // to save RELAY state accross power outage
#include <ArduinoJson.h> // 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 <PubSubClient.h> // Imroy MQTT library, set fix port to 8883 in source code!
#include <power.h> // for sonoff POW measurement routines
#include "DHT.h" // for sonoff TH10 which uses the temperature sensor DHT22
#include <ESP8266HTTPClient.h> // for Firmware Update using HTTP
#include <ESP8266httpUpdate.h> // ditto
const char* Version = "{\"Version\":\"Sonoff_Generic_v1i-2\"}";
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
PubSubClient mqttClient(TCP); // MQTT client object
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(const MQTT::Publish& pub) { // Callback is called when MQTT messages is received
String command = pub.payload_string(); // put payload into string for processing
StaticJsonBuffer<700> commandBuffer;
JsonObject& root = commandBuffer.parseObject(command);
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<char[]> 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.set_server(MQTT_Broker, 8883); // start config MQTT Server
mqttClient.set_callback(command_callback); // connection setup is done in main loop
input_topic = "input/" + String(Hostname);
cmd_topic = "command/" + String(Hostname); // end MQTT config
buttonTimer.attach(0.05, read_button); // reads button status 20 times per second
}
void loop() {
// if (millis() > 900000) { ESP.restart(); } // reboot every 15 min (=900000ms) (to fix unknown problems)
if (WiFi.status() != WL_CONNECTED) { // WiFi not connected,
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);
for ( int i = 0; i < 300; i++) { // try to connect to WiFi for max 30s
if (WiFi.status() == WL_CONNECTED) {break;}
delay(100);
}
WiFiRestart = true;
HostnameWifi = Hostname;
HostnameWifi.concat(".local");
WiFi.hostname(HostnameWifi); // 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
mqttClient.set_server(MQTT_Broker, 8883); // config MQTT Server
mqttClient.connect(MQTT::Connect(Hostname).set_auth(MQTT_User, MQTT_PW));
mqttClient.loop();
delay(1000);
if (!mqttClient.connected()) { // if MQTT still not connected, then
delay(5000); // wait 5 sec and reboot
ESP.restart();
} else { // MQTT connection succeeded, therefore
mqttClient.set_callback(command_callback); //
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;
}
} // at this point WiFi and MQTT are up and running
now = millis(); // Start measuring every 10 sec
if (now - lastMeasurement > 10000) {
mqttClient.loop();
yield();
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();
yield(); // end
}