/*
|
|
|
|
ITead Sonoff Custom Firmware
|
|
Copyright (C) 2016 by Xose Pérez <xose dot perez at gmail dot com>
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
#include <Arduino.h>
|
|
#include <ESP8266WiFi.h>
|
|
#include <ESP8266WebServer.h>
|
|
#include <ESP8266mDNS.h>
|
|
#include <PubSubClient.h>
|
|
#include "FS.h"
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Configuració
|
|
// -----------------------------------------------------------------------------
|
|
|
|
#define DEBUG
|
|
|
|
#define BUTTON_PIN 0
|
|
#define RELAY_PIN 12
|
|
#define LED_PIN 13
|
|
|
|
#define DEBOUNCE_COUNTER_START 150
|
|
#define AP_PASS "fibonacci"
|
|
#define BUFFER_SIZE 1024
|
|
#define CONFIG_PATH "/.config"
|
|
|
|
#define WIFI_CONNECT_TIMEOUT 5000
|
|
#define WIFI_RECONNECT_DELAY 30000
|
|
|
|
#define MQTT_RECONNECT_DELAY 30000
|
|
|
|
#define NETWORK_BUFFER 3
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Globals
|
|
// -----------------------------------------------------------------------------
|
|
|
|
ESP8266WebServer server(80);
|
|
WiFiClient client;
|
|
PubSubClient mqtt(client);
|
|
|
|
bool relayOn = false;
|
|
char identifier[] = "SONOFF_0000";
|
|
bool identifierSet = false;
|
|
|
|
byte network = 0;
|
|
String config_ssid[NETWORK_BUFFER];
|
|
String config_pass[NETWORK_BUFFER];
|
|
String mqtt_server = "192.168.1.100";
|
|
String mqtt_topic = "/test/switch/{identifier}";
|
|
String mqtt_port = "1883";
|
|
|
|
char mqtt_subscribe_to[30];
|
|
char mqtt_publish_to[30];
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Relay
|
|
// -----------------------------------------------------------------------------
|
|
|
|
void switchRelayOn() {
|
|
#ifdef DEBUG
|
|
Serial.println("Turning the relay ON");
|
|
#endif
|
|
if (mqtt.connected()) {
|
|
mqtt.publish(mqtt_publish_to, "1");
|
|
}
|
|
digitalWrite(RELAY_PIN, HIGH);
|
|
digitalWrite(LED_PIN, HIGH);
|
|
relayOn = true;
|
|
}
|
|
|
|
void switchRelayOff() {
|
|
#ifdef DEBUG
|
|
Serial.println("Turning the relay OFF");
|
|
#endif
|
|
if (mqtt.connected()) {
|
|
mqtt.publish(mqtt_publish_to, "0");
|
|
}
|
|
digitalWrite(RELAY_PIN, LOW);
|
|
digitalWrite(LED_PIN, LOW);
|
|
relayOn = false;
|
|
}
|
|
|
|
void toggleRelay() {
|
|
if (relayOn) {
|
|
switchRelayOff();
|
|
} else {
|
|
switchRelayOn();
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// WebServer
|
|
// -----------------------------------------------------------------------------
|
|
|
|
String getContentType(String filename) {
|
|
if (server.hasArg("download")) return "application/octet-stream";
|
|
else if (filename.endsWith(".htm")) return "text/html";
|
|
else if (filename.endsWith(".html")) return "text/html";
|
|
else if (filename.endsWith(".css")) return "text/css";
|
|
else if (filename.endsWith(".js")) return "application/javascript";
|
|
else if (filename.endsWith(".png")) return "image/png";
|
|
else if (filename.endsWith(".gif")) return "image/gif";
|
|
else if (filename.endsWith(".jpg")) return "image/jpeg";
|
|
else if (filename.endsWith(".ico")) return "image/x-icon";
|
|
else if (filename.endsWith(".xml")) return "text/xml";
|
|
else if (filename.endsWith(".pdf")) return "application/x-pdf";
|
|
else if (filename.endsWith(".zip")) return "application/x-zip";
|
|
else if (filename.endsWith(".gz")) return "application/x-gzip";
|
|
return "text/plain";
|
|
}
|
|
|
|
void handleRelayOn() {
|
|
#ifdef DEBUG
|
|
Serial.println("Request: /on");
|
|
#endif
|
|
switchRelayOn();
|
|
server.send(200, "text/plain", "ON");
|
|
}
|
|
|
|
void handleRelayOff() {
|
|
#ifdef DEBUG
|
|
Serial.println("Request: /off");
|
|
#endif
|
|
switchRelayOff();
|
|
server.send(200, "text/plain", "OFF");
|
|
}
|
|
|
|
bool handleFileRead(String path) {
|
|
|
|
#ifdef DEBUG
|
|
Serial.println("Request: " + path);
|
|
#endif
|
|
|
|
if (path.endsWith("/")) path += "index.html";
|
|
String contentType = getContentType(path);
|
|
String pathWithGz = path + ".gz";
|
|
|
|
if (SPIFFS.exists(pathWithGz)) path = pathWithGz;
|
|
if (SPIFFS.exists(path)) {
|
|
File file = SPIFFS.open(path, "r");
|
|
size_t sent = server.streamFile(file, contentType);
|
|
size_t contentLength = file.size();
|
|
file.close();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
void handleHome() {
|
|
|
|
#ifdef DEBUG
|
|
Serial.println("Request: /index.html");
|
|
#endif
|
|
|
|
String filename = "/index.html";
|
|
String content = "";
|
|
char buffer[BUFFER_SIZE];
|
|
|
|
// Read file in chunks
|
|
File file = SPIFFS.open(filename, "r");
|
|
int size = file.size();
|
|
while (size > 0) {
|
|
size_t len = std::min(BUFFER_SIZE-1, size);
|
|
file.read((uint8_t *) buffer, len);
|
|
buffer[len] = 0;
|
|
content += buffer;
|
|
size -= len;
|
|
}
|
|
file.close();
|
|
|
|
// Replace placeholders
|
|
if (WiFi.status() == WL_CONNECTED) {
|
|
content.replace("{status}", "Client + Acces Point");
|
|
content.replace("{network}", config_ssid[network]);
|
|
content.replace("{ip}", WiFi.localIP().toString());
|
|
} else {
|
|
content.replace("{status}", "Acces Point");
|
|
content.replace("{network}", "");
|
|
content.replace("{ip}", "");
|
|
}
|
|
content.replace("{ssid0}", config_ssid[0]);
|
|
content.replace("{pass0}", config_pass[0]);
|
|
content.replace("{ssid1}", config_ssid[1]);
|
|
content.replace("{pass1}", config_pass[1]);
|
|
content.replace("{ssid2}", config_ssid[2]);
|
|
content.replace("{pass2}", config_pass[2]);
|
|
content.replace("{mqtt_server}", mqtt_server);
|
|
content.replace("{mqtt_port}", mqtt_port);
|
|
content.replace("{mqtt_topic}", mqtt_topic);
|
|
|
|
// Serve content
|
|
String contentType = getContentType(filename);
|
|
server.send(200, contentType, content);
|
|
|
|
}
|
|
|
|
void handleSave() {
|
|
|
|
#ifdef DEBUG
|
|
Serial.println("Request: /save");
|
|
#endif
|
|
|
|
config_ssid[0] = server.arg("ssid0");
|
|
config_pass[0] = server.arg("pass0");
|
|
config_ssid[1] = server.arg("ssid1");
|
|
config_pass[1] = server.arg("pass1");
|
|
config_ssid[2] = server.arg("ssid2");
|
|
config_pass[2] = server.arg("pass2");
|
|
mqtt_server = server.arg("mqtt_server");
|
|
mqtt_port = server.arg("mqtt_port");
|
|
mqtt_topic = server.arg("mqtt_topic");
|
|
|
|
saveConfig();
|
|
network = 0;
|
|
wifiSetup();
|
|
delay(100);
|
|
|
|
String output = "{";
|
|
output += "\"status\": \"";
|
|
if (WiFi.status() == WL_CONNECTED) {
|
|
output += "Client + Acces Point";
|
|
} else {
|
|
output += "Acces Point";
|
|
}
|
|
output += "\", \"ip\": \"";
|
|
if (WiFi.status() == WL_CONNECTED) {
|
|
output += WiFi.localIP().toString();
|
|
}
|
|
output += "\" }";
|
|
server.send(200, "text/json", output);
|
|
|
|
}
|
|
|
|
void webServerSetup() {
|
|
|
|
// Relay control
|
|
server.on("/on", HTTP_GET, handleRelayOn);
|
|
server.on("/off", HTTP_GET, handleRelayOff);
|
|
|
|
// Configuration page
|
|
server.on("/save", HTTP_POST, handleSave);
|
|
server.on("/", HTTP_GET, handleHome);
|
|
server.on("/index.html", HTTP_GET, handleHome);
|
|
|
|
// Anything else
|
|
server.onNotFound([]() {
|
|
|
|
// Hidden files
|
|
if (server.uri().startsWith("/.")) {
|
|
server.send(403, "text/plain", "Forbidden");
|
|
return;
|
|
}
|
|
|
|
// Existing files in SPIFFS
|
|
if (!handleFileRead(server.uri())) {
|
|
server.send(404, "text/plain", "NotFound");
|
|
return;
|
|
}
|
|
|
|
});
|
|
|
|
// Run server
|
|
server.begin();
|
|
|
|
}
|
|
|
|
void webServerLoop() {
|
|
server.handleClient();
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Wifi modes
|
|
// -----------------------------------------------------------------------------
|
|
|
|
char * getIdentifier() {
|
|
if (!identifierSet) {
|
|
uint8_t mac[WL_MAC_ADDR_LENGTH];
|
|
WiFi.softAPmacAddress(mac);
|
|
String macID = String(mac[WL_MAC_ADDR_LENGTH - 2], HEX) + String(mac[WL_MAC_ADDR_LENGTH - 1], HEX);
|
|
macID.toUpperCase();
|
|
for (byte i=0; i<4; i++) {
|
|
identifier[7+i] = macID.charAt(i);
|
|
}
|
|
identifierSet = true;
|
|
}
|
|
return identifier;
|
|
}
|
|
|
|
void wifiSetup() {
|
|
|
|
// Disconnect MQTT
|
|
if (mqtt.connected()) mqtt.disconnect();
|
|
|
|
// STA mode
|
|
WiFi.mode(WIFI_AP_STA);
|
|
if (config_ssid[network].length() > 0) {
|
|
|
|
char ssid[config_ssid[network].length()+1];
|
|
char pass[config_pass[network].length()+1];
|
|
config_ssid[network].toCharArray(ssid, config_ssid[network].length()+1);
|
|
config_pass[network].toCharArray(pass, config_pass[network].length()+1);
|
|
WiFi.begin(ssid, pass);
|
|
|
|
#ifdef DEBUG
|
|
Serial.println("Connecting to WIFI " + config_ssid[network]);
|
|
#endif
|
|
|
|
// Wait
|
|
unsigned long timeout = millis() + WIFI_CONNECT_TIMEOUT;
|
|
while (timeout > millis()) {
|
|
if (WiFi.status() == WL_CONNECTED) break;
|
|
delay(100);
|
|
}
|
|
|
|
#ifdef DEBUG
|
|
Serial.print("STA Mode: ");
|
|
Serial.print(config_ssid[network]);
|
|
Serial.print("/");
|
|
Serial.print(config_pass[network]);
|
|
Serial.print(", IP address: ");
|
|
#endif
|
|
|
|
if (WiFi.status() == WL_CONNECTED) {
|
|
#ifdef DEBUG
|
|
Serial.println(WiFi.localIP());
|
|
#endif
|
|
} else {
|
|
network = (network + 1) % NETWORK_BUFFER;
|
|
#ifdef DEBUG
|
|
Serial.println("NOT CONNECTED");
|
|
#endif
|
|
}
|
|
|
|
}
|
|
|
|
if (WiFi.status() != WL_CONNECTED) WiFi.mode(WIFI_AP);
|
|
WiFi.softAP(getIdentifier(), AP_PASS);
|
|
|
|
#ifdef DEBUG
|
|
Serial.print("AP Mode: ");
|
|
Serial.print(getIdentifier());
|
|
Serial.print("/");
|
|
Serial.print(AP_PASS);
|
|
Serial.print(", IP address: ");
|
|
Serial.println(WiFi.softAPIP());
|
|
#endif
|
|
|
|
}
|
|
|
|
void wifiLoop() {
|
|
static unsigned long timeout = millis();
|
|
if (WiFi.status() != WL_CONNECTED) {
|
|
if (timeout < millis()) {
|
|
wifiSetup();
|
|
timeout = millis() + WIFI_RECONNECT_DELAY;
|
|
}
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// MQTT
|
|
// -----------------------------------------------------------------------------
|
|
|
|
void buildTopics() {
|
|
|
|
// Replace identifier
|
|
String base = mqtt_topic;
|
|
base.replace("{identifier}", getIdentifier());
|
|
|
|
// Get publish topic
|
|
base.toCharArray(mqtt_publish_to, base.length()+1);
|
|
mqtt_publish_to[base.length()+1] = 0;
|
|
|
|
// Get subscribe topic
|
|
String subscribe = base + "/set";
|
|
subscribe.toCharArray(mqtt_subscribe_to, subscribe.length()+1);
|
|
mqtt_subscribe_to[subscribe.length()+1] = 0;
|
|
|
|
}
|
|
|
|
void mqttCallback(char* topic, byte* payload, unsigned int length) {
|
|
|
|
#ifdef DEBUG
|
|
Serial.print("MQTT message ");
|
|
Serial.print(topic);
|
|
Serial.print(" => ");
|
|
for (int i = 0; i < length; i++) {
|
|
Serial.print((char)payload[i]);
|
|
}
|
|
Serial.println();
|
|
#endif
|
|
|
|
if ((char)payload[0] == '1') {
|
|
switchRelayOn();
|
|
} else {
|
|
switchRelayOff();
|
|
}
|
|
|
|
}
|
|
|
|
void mqttConnect() {
|
|
|
|
if (!mqtt.connected()) {
|
|
|
|
char buffer[mqtt_server.length()+1];
|
|
mqtt_server.toCharArray(buffer, mqtt_server.length()+1);
|
|
mqtt.setServer(buffer, mqtt_port.toInt());
|
|
|
|
#ifdef DEBUG
|
|
Serial.print("Connecting to MQTT broker: ");
|
|
#endif
|
|
|
|
if (mqtt.connect(getIdentifier())) {
|
|
|
|
buildTopics();
|
|
|
|
#ifdef DEBUG
|
|
Serial.println("connected!");
|
|
Serial.print("Subscribing to ");
|
|
Serial.println(mqtt_subscribe_to);
|
|
#endif
|
|
|
|
mqtt.subscribe(mqtt_subscribe_to);
|
|
|
|
|
|
} else {
|
|
|
|
#ifdef DEBUG
|
|
Serial.print("failed, rc=");
|
|
Serial.println(mqtt.state());
|
|
#endif
|
|
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void mqttSetup() {
|
|
mqtt.setCallback(mqttCallback);
|
|
}
|
|
|
|
void mqttLoop() {
|
|
static unsigned long timeout = millis();
|
|
if (WiFi.status() == WL_CONNECTED) {
|
|
if (!mqtt.connected()) {
|
|
if (timeout < millis()) {
|
|
mqttConnect();
|
|
timeout = millis() + MQTT_RECONNECT_DELAY;
|
|
}
|
|
}
|
|
if (mqtt.connected()) mqtt.loop();
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Configuration
|
|
// -----------------------------------------------------------------------------
|
|
|
|
bool saveConfig() {
|
|
File file = SPIFFS.open(CONFIG_PATH, "w");
|
|
if (file) {
|
|
file.println("ssid0=" + config_ssid[0]);
|
|
file.println("pass0=" + config_pass[0]);
|
|
file.println("ssid1=" + config_ssid[1]);
|
|
file.println("pass1=" + config_pass[1]);
|
|
file.println("ssid2=" + config_ssid[2]);
|
|
file.println("pass2=" + config_pass[2]);
|
|
file.println("mqtt_server=" + mqtt_server);
|
|
file.println("mqtt_port=" + mqtt_port);
|
|
file.println("mqtt_topic=" + mqtt_topic);
|
|
file.close();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool loadConfig() {
|
|
|
|
if (SPIFFS.exists(CONFIG_PATH)) {
|
|
|
|
#ifdef DEBUG
|
|
Serial.println("Reading config file");
|
|
#endif
|
|
|
|
// Read contents
|
|
File file = SPIFFS.open(CONFIG_PATH, "r");
|
|
String content = file.readString();
|
|
file.close();
|
|
|
|
// Parse contents
|
|
content.replace("\r\n", "\n");
|
|
content.replace("\r", "\n");
|
|
|
|
int start = 0;
|
|
int end = content.indexOf("\n", start);
|
|
while (end > 0) {
|
|
String line = content.substring(start, end);
|
|
#ifdef DEBUG
|
|
Serial.println(line);
|
|
#endif
|
|
if (line.startsWith("ssid0=")) config_ssid[0] = line.substring(6);
|
|
else if (line.startsWith("pass0=")) config_pass[0] = line.substring(6);
|
|
else if (line.startsWith("ssid1=")) config_ssid[1] = line.substring(6);
|
|
else if (line.startsWith("pass1=")) config_pass[1] = line.substring(6);
|
|
else if (line.startsWith("ssid2=")) config_ssid[2] = line.substring(6);
|
|
else if (line.startsWith("pass2=")) config_pass[2] = line.substring(6);
|
|
else if (line.startsWith("mqtt_server=")) mqtt_server = line.substring(12);
|
|
else if (line.startsWith("mqtt_port=")) mqtt_port = line.substring(10);
|
|
else if (line.startsWith("mqtt_topic=")) mqtt_topic = line.substring(11);
|
|
if (end < 0) break;
|
|
start = end + 1;
|
|
end = content.indexOf("\n", start);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
return false;
|
|
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Generic methods
|
|
// -----------------------------------------------------------------------------
|
|
|
|
void hardwareSetup() {
|
|
Serial.begin(115200);
|
|
pinMode(RELAY_PIN, OUTPUT);
|
|
pinMode(BUTTON_PIN, INPUT_PULLUP);
|
|
pinMode(LED_PIN, OUTPUT);
|
|
SPIFFS.begin();
|
|
}
|
|
|
|
void buttonLoop() {
|
|
|
|
static int lastButtonState = HIGH;
|
|
static int debounceCounter = 0;
|
|
|
|
if (debounceCounter > 0) {
|
|
if (debounceCounter == 1) {
|
|
int newButtonState = lastButtonState == HIGH ? LOW : HIGH;
|
|
if (newButtonState == LOW) {
|
|
toggleRelay();
|
|
}
|
|
lastButtonState = newButtonState;
|
|
}
|
|
debounceCounter--;
|
|
|
|
} else if (lastButtonState != digitalRead(BUTTON_PIN)) {
|
|
debounceCounter = DEBOUNCE_COUNTER_START;
|
|
}
|
|
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Booting
|
|
// -----------------------------------------------------------------------------
|
|
|
|
void setup() {
|
|
hardwareSetup();
|
|
delay(5000);
|
|
switchRelayOff();
|
|
loadConfig();
|
|
wifiSetup();
|
|
webServerSetup();
|
|
mqttSetup();
|
|
}
|
|
|
|
void loop() {
|
|
wifiLoop();
|
|
webServerLoop();
|
|
mqttLoop();
|
|
buttonLoop();
|
|
delay(1);
|
|
}
|