Fork of the espurna firmware for `mhsw` switches
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

593 lines
16 KiB

8 years ago
  1. /*
  2. ITead Sonoff Custom Firmware
  3. Copyright (C) 2016 by Xose Pérez <xose dot perez at gmail dot com>
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License
  13. along with this program. If not, see <http://www.gnu.org/licenses/>.
  14. */
  15. #include <Arduino.h>
  16. #include <ESP8266WiFi.h>
  17. #include <ESP8266WebServer.h>
  18. #include <ESP8266mDNS.h>
  19. #include <PubSubClient.h>
  20. #include "FS.h"
  21. // -----------------------------------------------------------------------------
  22. // Configuració
  23. // -----------------------------------------------------------------------------
  24. #define DEBUG
  25. #define BUTTON_PIN 0
  26. #define RELAY_PIN 12
  27. #define LED_PIN 13
  28. #define DEBOUNCE_COUNTER_START 150
  29. #define AP_PASS "fibonacci"
  30. #define BUFFER_SIZE 1024
  31. #define CONFIG_PATH "/.config"
  32. #define WIFI_CONNECT_TIMEOUT 5000
  33. #define WIFI_RECONNECT_DELAY 30000
  34. #define MQTT_RECONNECT_DELAY 30000
  35. #define NETWORK_BUFFER 3
  36. // -----------------------------------------------------------------------------
  37. // Globals
  38. // -----------------------------------------------------------------------------
  39. ESP8266WebServer server(80);
  40. WiFiClient client;
  41. PubSubClient mqtt(client);
  42. bool relayOn = false;
  43. char identifier[] = "SONOFF_0000";
  44. bool identifierSet = false;
  45. byte network = 0;
  46. String config_ssid[NETWORK_BUFFER];
  47. String config_pass[NETWORK_BUFFER];
  48. String mqtt_server = "192.168.1.100";
  49. String mqtt_topic = "/test/switch/{identifier}";
  50. String mqtt_port = "1883";
  51. char mqtt_subscribe_to[30];
  52. char mqtt_publish_to[30];
  53. // -----------------------------------------------------------------------------
  54. // Relay
  55. // -----------------------------------------------------------------------------
  56. void switchRelayOn() {
  57. #ifdef DEBUG
  58. Serial.println("Turning the relay ON");
  59. #endif
  60. if (mqtt.connected()) {
  61. mqtt.publish(mqtt_publish_to, "1");
  62. }
  63. digitalWrite(RELAY_PIN, HIGH);
  64. digitalWrite(LED_PIN, HIGH);
  65. relayOn = true;
  66. }
  67. void switchRelayOff() {
  68. #ifdef DEBUG
  69. Serial.println("Turning the relay OFF");
  70. #endif
  71. if (mqtt.connected()) {
  72. mqtt.publish(mqtt_publish_to, "0");
  73. }
  74. digitalWrite(RELAY_PIN, LOW);
  75. digitalWrite(LED_PIN, LOW);
  76. relayOn = false;
  77. }
  78. void toggleRelay() {
  79. if (relayOn) {
  80. switchRelayOff();
  81. } else {
  82. switchRelayOn();
  83. }
  84. }
  85. // -----------------------------------------------------------------------------
  86. // WebServer
  87. // -----------------------------------------------------------------------------
  88. String getContentType(String filename) {
  89. if (server.hasArg("download")) return "application/octet-stream";
  90. else if (filename.endsWith(".htm")) return "text/html";
  91. else if (filename.endsWith(".html")) return "text/html";
  92. else if (filename.endsWith(".css")) return "text/css";
  93. else if (filename.endsWith(".js")) return "application/javascript";
  94. else if (filename.endsWith(".png")) return "image/png";
  95. else if (filename.endsWith(".gif")) return "image/gif";
  96. else if (filename.endsWith(".jpg")) return "image/jpeg";
  97. else if (filename.endsWith(".ico")) return "image/x-icon";
  98. else if (filename.endsWith(".xml")) return "text/xml";
  99. else if (filename.endsWith(".pdf")) return "application/x-pdf";
  100. else if (filename.endsWith(".zip")) return "application/x-zip";
  101. else if (filename.endsWith(".gz")) return "application/x-gzip";
  102. return "text/plain";
  103. }
  104. void handleRelayOn() {
  105. #ifdef DEBUG
  106. Serial.println("Request: /on");
  107. #endif
  108. switchRelayOn();
  109. server.send(200, "text/plain", "ON");
  110. }
  111. void handleRelayOff() {
  112. #ifdef DEBUG
  113. Serial.println("Request: /off");
  114. #endif
  115. switchRelayOff();
  116. server.send(200, "text/plain", "OFF");
  117. }
  118. bool handleFileRead(String path) {
  119. #ifdef DEBUG
  120. Serial.println("Request: " + path);
  121. #endif
  122. if (path.endsWith("/")) path += "index.html";
  123. String contentType = getContentType(path);
  124. String pathWithGz = path + ".gz";
  125. if (SPIFFS.exists(pathWithGz)) path = pathWithGz;
  126. if (SPIFFS.exists(path)) {
  127. File file = SPIFFS.open(path, "r");
  128. size_t sent = server.streamFile(file, contentType);
  129. size_t contentLength = file.size();
  130. file.close();
  131. return true;
  132. }
  133. return false;
  134. }
  135. void handleHome() {
  136. #ifdef DEBUG
  137. Serial.println("Request: /index.html");
  138. #endif
  139. String filename = "/index.html";
  140. String content = "";
  141. char buffer[BUFFER_SIZE];
  142. // Read file in chunks
  143. File file = SPIFFS.open(filename, "r");
  144. int size = file.size();
  145. while (size > 0) {
  146. size_t len = std::min(BUFFER_SIZE-1, size);
  147. file.read((uint8_t *) buffer, len);
  148. buffer[len] = 0;
  149. content += buffer;
  150. size -= len;
  151. }
  152. file.close();
  153. // Replace placeholders
  154. if (WiFi.status() == WL_CONNECTED) {
  155. content.replace("{status}", "Client + Acces Point");
  156. content.replace("{network}", config_ssid[network]);
  157. content.replace("{ip}", WiFi.localIP().toString());
  158. } else {
  159. content.replace("{status}", "Acces Point");
  160. content.replace("{network}", "");
  161. content.replace("{ip}", "");
  162. }
  163. content.replace("{ssid0}", config_ssid[0]);
  164. content.replace("{pass0}", config_pass[0]);
  165. content.replace("{ssid1}", config_ssid[1]);
  166. content.replace("{pass1}", config_pass[1]);
  167. content.replace("{ssid2}", config_ssid[2]);
  168. content.replace("{pass2}", config_pass[2]);
  169. content.replace("{mqtt_server}", mqtt_server);
  170. content.replace("{mqtt_port}", mqtt_port);
  171. content.replace("{mqtt_topic}", mqtt_topic);
  172. // Serve content
  173. String contentType = getContentType(filename);
  174. server.send(200, contentType, content);
  175. }
  176. void handleSave() {
  177. #ifdef DEBUG
  178. Serial.println("Request: /save");
  179. #endif
  180. config_ssid[0] = server.arg("ssid0");
  181. config_pass[0] = server.arg("pass0");
  182. config_ssid[1] = server.arg("ssid1");
  183. config_pass[1] = server.arg("pass1");
  184. config_ssid[2] = server.arg("ssid2");
  185. config_pass[2] = server.arg("pass2");
  186. mqtt_server = server.arg("mqtt_server");
  187. mqtt_port = server.arg("mqtt_port");
  188. mqtt_topic = server.arg("mqtt_topic");
  189. saveConfig();
  190. network = 0;
  191. wifiSetup();
  192. delay(100);
  193. String output = "{";
  194. output += "\"status\": \"";
  195. if (WiFi.status() == WL_CONNECTED) {
  196. output += "Client + Acces Point";
  197. } else {
  198. output += "Acces Point";
  199. }
  200. output += "\", \"ip\": \"";
  201. if (WiFi.status() == WL_CONNECTED) {
  202. output += WiFi.localIP().toString();
  203. }
  204. output += "\" }";
  205. server.send(200, "text/json", output);
  206. }
  207. void webServerSetup() {
  208. // Relay control
  209. server.on("/on", HTTP_GET, handleRelayOn);
  210. server.on("/off", HTTP_GET, handleRelayOff);
  211. // Configuration page
  212. server.on("/save", HTTP_POST, handleSave);
  213. server.on("/", HTTP_GET, handleHome);
  214. server.on("/index.html", HTTP_GET, handleHome);
  215. // Anything else
  216. server.onNotFound([]() {
  217. // Hidden files
  218. if (server.uri().startsWith("/.")) {
  219. server.send(403, "text/plain", "Forbidden");
  220. return;
  221. }
  222. // Existing files in SPIFFS
  223. if (!handleFileRead(server.uri())) {
  224. server.send(404, "text/plain", "NotFound");
  225. return;
  226. }
  227. });
  228. // Run server
  229. server.begin();
  230. }
  231. void webServerLoop() {
  232. server.handleClient();
  233. }
  234. // -----------------------------------------------------------------------------
  235. // Wifi modes
  236. // -----------------------------------------------------------------------------
  237. char * getIdentifier() {
  238. if (!identifierSet) {
  239. uint8_t mac[WL_MAC_ADDR_LENGTH];
  240. WiFi.softAPmacAddress(mac);
  241. String macID = String(mac[WL_MAC_ADDR_LENGTH - 2], HEX) + String(mac[WL_MAC_ADDR_LENGTH - 1], HEX);
  242. macID.toUpperCase();
  243. for (byte i=0; i<4; i++) {
  244. identifier[7+i] = macID.charAt(i);
  245. }
  246. identifierSet = true;
  247. }
  248. return identifier;
  249. }
  250. void wifiSetup() {
  251. // Disconnect MQTT
  252. if (mqtt.connected()) mqtt.disconnect();
  253. // STA mode
  254. WiFi.mode(WIFI_AP_STA);
  255. if (config_ssid[network].length() > 0) {
  256. char ssid[config_ssid[network].length()+1];
  257. char pass[config_pass[network].length()+1];
  258. config_ssid[network].toCharArray(ssid, config_ssid[network].length()+1);
  259. config_pass[network].toCharArray(pass, config_pass[network].length()+1);
  260. WiFi.begin(ssid, pass);
  261. #ifdef DEBUG
  262. Serial.println("Connecting to WIFI " + config_ssid[network]);
  263. #endif
  264. // Wait
  265. unsigned long timeout = millis() + WIFI_CONNECT_TIMEOUT;
  266. while (timeout > millis()) {
  267. if (WiFi.status() == WL_CONNECTED) break;
  268. delay(100);
  269. }
  270. #ifdef DEBUG
  271. Serial.print("STA Mode: ");
  272. Serial.print(config_ssid[network]);
  273. Serial.print("/");
  274. Serial.print(config_pass[network]);
  275. Serial.print(", IP address: ");
  276. #endif
  277. if (WiFi.status() == WL_CONNECTED) {
  278. #ifdef DEBUG
  279. Serial.println(WiFi.localIP());
  280. #endif
  281. } else {
  282. network = (network + 1) % NETWORK_BUFFER;
  283. #ifdef DEBUG
  284. Serial.println("NOT CONNECTED");
  285. #endif
  286. }
  287. }
  288. if (WiFi.status() != WL_CONNECTED) WiFi.mode(WIFI_AP);
  289. WiFi.softAP(getIdentifier(), AP_PASS);
  290. #ifdef DEBUG
  291. Serial.print("AP Mode: ");
  292. Serial.print(getIdentifier());
  293. Serial.print("/");
  294. Serial.print(AP_PASS);
  295. Serial.print(", IP address: ");
  296. Serial.println(WiFi.softAPIP());
  297. #endif
  298. }
  299. void wifiLoop() {
  300. static unsigned long timeout = millis();
  301. if (WiFi.status() != WL_CONNECTED) {
  302. if (timeout < millis()) {
  303. wifiSetup();
  304. timeout = millis() + WIFI_RECONNECT_DELAY;
  305. }
  306. }
  307. }
  308. // -----------------------------------------------------------------------------
  309. // MQTT
  310. // -----------------------------------------------------------------------------
  311. void buildTopics() {
  312. // Replace identifier
  313. String base = mqtt_topic;
  314. base.replace("{identifier}", getIdentifier());
  315. // Get publish topic
  316. base.toCharArray(mqtt_publish_to, base.length()+1);
  317. mqtt_publish_to[base.length()+1] = 0;
  318. // Get subscribe topic
  319. String subscribe = base + "/set";
  320. subscribe.toCharArray(mqtt_subscribe_to, subscribe.length()+1);
  321. mqtt_subscribe_to[subscribe.length()+1] = 0;
  322. }
  323. void mqttCallback(char* topic, byte* payload, unsigned int length) {
  324. #ifdef DEBUG
  325. Serial.print("MQTT message ");
  326. Serial.print(topic);
  327. Serial.print(" => ");
  328. for (int i = 0; i < length; i++) {
  329. Serial.print((char)payload[i]);
  330. }
  331. Serial.println();
  332. #endif
  333. if ((char)payload[0] == '1') {
  334. switchRelayOn();
  335. } else {
  336. switchRelayOff();
  337. }
  338. }
  339. void mqttConnect() {
  340. if (!mqtt.connected()) {
  341. char buffer[mqtt_server.length()+1];
  342. mqtt_server.toCharArray(buffer, mqtt_server.length()+1);
  343. mqtt.setServer(buffer, mqtt_port.toInt());
  344. #ifdef DEBUG
  345. Serial.print("Connecting to MQTT broker: ");
  346. #endif
  347. if (mqtt.connect(getIdentifier())) {
  348. buildTopics();
  349. #ifdef DEBUG
  350. Serial.println("connected!");
  351. Serial.print("Subscribing to ");
  352. Serial.println(mqtt_subscribe_to);
  353. #endif
  354. mqtt.subscribe(mqtt_subscribe_to);
  355. } else {
  356. #ifdef DEBUG
  357. Serial.print("failed, rc=");
  358. Serial.println(mqtt.state());
  359. #endif
  360. }
  361. }
  362. }
  363. void mqttSetup() {
  364. mqtt.setCallback(mqttCallback);
  365. }
  366. void mqttLoop() {
  367. static unsigned long timeout = millis();
  368. if (WiFi.status() == WL_CONNECTED) {
  369. if (!mqtt.connected()) {
  370. if (timeout < millis()) {
  371. mqttConnect();
  372. timeout = millis() + MQTT_RECONNECT_DELAY;
  373. }
  374. }
  375. if (mqtt.connected()) mqtt.loop();
  376. }
  377. }
  378. // -----------------------------------------------------------------------------
  379. // Configuration
  380. // -----------------------------------------------------------------------------
  381. bool saveConfig() {
  382. File file = SPIFFS.open(CONFIG_PATH, "w");
  383. if (file) {
  384. file.println("ssid0=" + config_ssid[0]);
  385. file.println("pass0=" + config_pass[0]);
  386. file.println("ssid1=" + config_ssid[1]);
  387. file.println("pass1=" + config_pass[1]);
  388. file.println("ssid2=" + config_ssid[2]);
  389. file.println("pass2=" + config_pass[2]);
  390. file.println("mqtt_server=" + mqtt_server);
  391. file.println("mqtt_port=" + mqtt_port);
  392. file.println("mqtt_topic=" + mqtt_topic);
  393. file.close();
  394. return true;
  395. }
  396. return false;
  397. }
  398. bool loadConfig() {
  399. if (SPIFFS.exists(CONFIG_PATH)) {
  400. #ifdef DEBUG
  401. Serial.println("Reading config file");
  402. #endif
  403. // Read contents
  404. File file = SPIFFS.open(CONFIG_PATH, "r");
  405. String content = file.readString();
  406. file.close();
  407. // Parse contents
  408. content.replace("\r\n", "\n");
  409. content.replace("\r", "\n");
  410. int start = 0;
  411. int end = content.indexOf("\n", start);
  412. while (end > 0) {
  413. String line = content.substring(start, end);
  414. #ifdef DEBUG
  415. Serial.println(line);
  416. #endif
  417. if (line.startsWith("ssid0=")) config_ssid[0] = line.substring(6);
  418. else if (line.startsWith("pass0=")) config_pass[0] = line.substring(6);
  419. else if (line.startsWith("ssid1=")) config_ssid[1] = line.substring(6);
  420. else if (line.startsWith("pass1=")) config_pass[1] = line.substring(6);
  421. else if (line.startsWith("ssid2=")) config_ssid[2] = line.substring(6);
  422. else if (line.startsWith("pass2=")) config_pass[2] = line.substring(6);
  423. else if (line.startsWith("mqtt_server=")) mqtt_server = line.substring(12);
  424. else if (line.startsWith("mqtt_port=")) mqtt_port = line.substring(10);
  425. else if (line.startsWith("mqtt_topic=")) mqtt_topic = line.substring(11);
  426. if (end < 0) break;
  427. start = end + 1;
  428. end = content.indexOf("\n", start);
  429. }
  430. return true;
  431. }
  432. return false;
  433. }
  434. // -----------------------------------------------------------------------------
  435. // Generic methods
  436. // -----------------------------------------------------------------------------
  437. void hardwareSetup() {
  438. Serial.begin(115200);
  439. pinMode(RELAY_PIN, OUTPUT);
  440. pinMode(BUTTON_PIN, INPUT_PULLUP);
  441. pinMode(LED_PIN, OUTPUT);
  442. SPIFFS.begin();
  443. }
  444. void buttonLoop() {
  445. static int lastButtonState = HIGH;
  446. static int debounceCounter = 0;
  447. if (debounceCounter > 0) {
  448. if (debounceCounter == 1) {
  449. int newButtonState = lastButtonState == HIGH ? LOW : HIGH;
  450. if (newButtonState == LOW) {
  451. toggleRelay();
  452. }
  453. lastButtonState = newButtonState;
  454. }
  455. debounceCounter--;
  456. } else if (lastButtonState != digitalRead(BUTTON_PIN)) {
  457. debounceCounter = DEBOUNCE_COUNTER_START;
  458. }
  459. }
  460. // -----------------------------------------------------------------------------
  461. // Booting
  462. // -----------------------------------------------------------------------------
  463. void setup() {
  464. hardwareSetup();
  465. delay(5000);
  466. switchRelayOff();
  467. loadConfig();
  468. wifiSetup();
  469. webServerSetup();
  470. mqttSetup();
  471. }
  472. void loop() {
  473. wifiLoop();
  474. webServerLoop();
  475. mqttLoop();
  476. buttonLoop();
  477. delay(1);
  478. }