Cactus

V1.0

๐ŸŒต Measure humidity in kitchen cupboards to keep food fresh ๐Ÿถ

power LiPO
wireless WiFi
sensor Humidity
mcu ESP8266
ota? No
battery life 1 month
bom items 36
bom cost USD $26.92
vendors 5
status completed in May 2019

Features

  • Read humidity and temperature values from the sensor Si7021
  • Read LiPo battery levels
  • Display the current humidity value through the on-board LEDs
  • Wakeup every 6 hours or on button press to read the sensor and send data to the cloud
  • Sleep after sending data to IFTTT
  • Check for stored WiFi credentials upon waking up and connect back to the store WiFi
  • Connect to WiFi with the stored credentials
  • Setup an AP mode if WiFi cannot be connected
  • Send sensor and battery values to IFTTT if WiFi is connected
  • Wait for 5 minutes in AP mode is WiFi is not connected
  • Sleep after waiting for 5 minutes in AP mode
  • Charge the LiPo by plugging in the USB cable into the battery shield

Firmware

Download code

Use firmware version at least Arduino ESP8266 commit hash 0da69064 and above for mDNS patch.

#include <EEPROM.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <Ticker.h>
#include <math.h>
#include "Adafruit_Si7021.h"
#include "./web.h"

// Things to input
// Wakeup every 10,800,000,000 ยตs = 10800e6 = 10,800s = 3 hours
#define SLEEP_INTERVAL_DURATION  10800e6
#define SLEEP_DURATION_ENGLISH String("3 hours")
#define MAX_SLEEP_COUNT 2  // 3 hours * 2 times = 6 hours

// Options
#define DEBUG true
// Change to true if WiFi config need to be erased for a new SSID
// Change back to false and flash the firmware immediately
// TODO: Implement a factory reset button to erase all stored configurations
bool willEraseWiFiCredentials = false;

// Other constants
#define EN 2  // GPIO02 on ESP-01 module, D4 on nodeMCU WeMos, or on-board LED
#define LED 2
#define BATTERY_VOLT A0
#define USERBUTTON 12  // GPIO012 on ESP or D6 on WeMos
#define CURRENT_SLEEP_COUNT EEPROM.read(CURRENT_SLEEP_INTERVAL_ADDR)
#define IFTTT_KEY_LENGTH 22
#define CURRENT_SLEEP_INTERVAL_ADDR 30  // EEPROM add. to store sleep interval
#define MAX_WIFI_RECONNECT_INTERVAL 20  // WiFi will try to connect for 20s
#define MAX_AP_ON_MINUTES 5  // The AP mode will be on for 5 minutes

// WiFi
char ssid[50] = "secret";
char password[50] = "secret";
String AP_NamePrefix = "Cactus ";
const char WiFiAPPSK[] = "hutscape";
const char* DomainName = "cactus";  // set domain name domain.local

// LEDs and shift register
int dataPin = 13;  // pin D7 `GPIO13` on NodeMCU boards
int clockPin = 14;  // pin D5 `GPIO14` on NodeMCU boards
int latchPin = 15;  // pin D8 `GPIO15` on NodeMCU boards

struct SensorValues {
  float temperature;
  float humidity;
};

int userButtonValue = 1;
bool isAPWebServerRunning = false;
int apLoopCount = 0;
Ticker ticker;
SensorValues sensorValues;

const char* host = "maker.ifttt.com";
const int httpsPort = 443;
const int httpPort = 80;

// Get fingerprint of maker.ifttt.com
// echo | openssl s_client -connect maker.ifttt.com:443 |& openssl x509 -fingerprint -noout
const char fingerprint[] PROGMEM = "AA:75:CB:41:2E:D5:F9:97:FF:5D:A0:8B:7D:AC:12:21:08:4B:00:8C";

ESP8266WebServer server(80);
Adafruit_Si7021 sensor = Adafruit_Si7021();

void setup() {
  EEPROM.begin(512);
  Serial.begin(115200);

  disableWiFi();
  debugPrintln("");
  userButtonValue = digitalRead(USERBUTTON);

  debugPrintln("[INFO] Wakeup sleep count " + String(CURRENT_SLEEP_COUNT) + "/" + String(MAX_SLEEP_COUNT));

  if (willEraseWiFiCredentials) {
    eraseWiFiCredentials();
    debugPrintln("[INFO] Erasing WiFi credentials");
    debugPrintln("[INFO] Read empty WiFi SSID: " + WiFi.SSID());
  }

  if (!isCurrentSleepCountMax() && !hasUserPressedButton(userButtonValue)) {
    increaseSleepCount();
    debugPrintln("[INFO] Going into deep sleep for " + SLEEP_DURATION_ENGLISH);
    goToSleep();
    return;
  }

  initReadingBatteryVoltage();
  initTempHumiditySensor();
  resetSleepCount();
  sensorValues = readTempHumidity();

  if (hasUserPressedButton(userButtonValue)) {
    debugPrintln("[INFO] Wakeup on user button press!");
    initShiftRegister();
    displayHumidity(sensorValues.humidity);
  }

  enableWiFi();
  bool isConnectedToWiFi = connectToWiFi(true);
  if (isConnectedToWiFi) {
    debugPrintln("[INFO] Connected succesfully to WiFi SSID " + WiFi.SSID());
    debugPrintln("[INFO] WiFi connected! IP address: " + WiFi.localIP().toString());
  } else {
    debugPrintln("[ERROR] Connection to WiFi failed");
    debugPrintln("[INFO] Configuring access point");
    initAccessPoint();
    debugPrintln("[INFO] Started access point at IP " + WiFi.softAPIP().toString());

    bool hasMDNSStarted = MDNS.begin(DomainName, WiFi.softAPIP());
    if (!hasMDNSStarted) {
      debugPrintln("[ERROR] mDNS has failed to start");
    }

    startServer();
    debugPrintln("[INFO] WiFi is not configured!");
    debugPrintln("[INFO] Connect to SSID 'Cactus NNNN'");
    debugPrintln("[INFO] Go to http://cactus.local");

    // Start blinking LED to indicate AP mode
    pinMode(LED_BUILTIN, OUTPUT);
    ticker.attach(1, blink);
  }
}

void loop() {
  if (isAPWebServerRunning) {
    MDNS.update();
    server.handleClient();

    if (++apLoopCount > MAX_AP_ON_MINUTES*600) {
      debugPrintln("[INFO] Going into deep sleep for " + SLEEP_DURATION_ENGLISH);
      goToSleep();
    }
    delay(100);
  } else {
    ticker.detach();

    sendToIFTTT(sensorValues, getBatteryVoltage());
    debugPrintln("");
    debugPrintln("[INFO] Going into deep sleep after task for " + SLEEP_DURATION_ENGLISH);
    goToSleep();
  }
}

// Print functions
void debugPrintln(String s) {
  if (DEBUG) {
    Serial.println(s);
  }
}

void debugPrint(String s) {
  if (DEBUG) {
    Serial.print(s);
  }
}

// Sleep functions
bool isCurrentSleepCountMax() {
  return CURRENT_SLEEP_COUNT >= MAX_SLEEP_COUNT;
}

void increaseSleepCount() {
  EEPROM.write(CURRENT_SLEEP_INTERVAL_ADDR, CURRENT_SLEEP_COUNT + 1);
  EEPROM.commit();
}

void resetSleepCount() {
  EEPROM.write(CURRENT_SLEEP_INTERVAL_ADDR, 1);
  EEPROM.commit();
}

// EEPROM
String readKey() {
  String readStr;
  char readChar;

  for (int i = 0; i < IFTTT_KEY_LENGTH; ++i) {
    readChar = char(EEPROM.read(i));
    readStr += readChar;
  }

  return readStr;
}

String readEvent() {
  String word;
  char readChar;

  // IFTTT Event name is stored after the IFTTT Key in EEPROM
  int i = IFTTT_KEY_LENGTH;

  while (readChar != '\0') {
    readChar = char(EEPROM.read(i));
    delay(10);
    i++;

    if (readChar != '\0') {
      word += readChar;
    }
  }

  return word;
}

void writeKey(String writeStr) {
  delay(10);

  for (int i = 0; i < writeStr.length(); ++i) {
    EEPROM.write(i, writeStr[i]);
  }

  EEPROM.commit();
}

void writeEvent(String eventName) {
  delay(10);

  int address = 0;
  int eventNameIndex = 0;
  int initialAddress = IFTTT_KEY_LENGTH;
  int finalAddress = IFTTT_KEY_LENGTH + eventName.length();

  for (address = initialAddress; address < finalAddress; ++address) {
    delay(10);

    EEPROM.write(address, eventName[eventNameIndex]);
    eventNameIndex++;
  }

  EEPROM.write(finalAddress, '\0');
  EEPROM.commit();
}

// Battery
float getBatteryVoltage() {
  unsigned int raw = 0;
  float volt = 0.0;
  float batteryVoltage = 0.0;

  // NOTE: Get 10 analog values and smoothen it
  for (int count = 0; count < 10; count++) {
    raw = analogRead(A0);
    volt = raw / 1023.0;
    volt *= 4.2;

    batteryVoltage += volt;
  }

  batteryVoltage /= 10;

  Serial.print("[INFO] Battery voltage is ");
  Serial.print(batteryVoltage);
  Serial.println("V");

  return volt;
}

// Sensor functions
bool hasUserPressedButton(int userButtonValue) {
  return userButtonValue == 0;
}

void blink() {
  int state = digitalRead(LED_BUILTIN);
  digitalWrite(LED_BUILTIN, !state);
}

void ledOFF() {
  digitalWrite(LED_BUILTIN, HIGH);
}

void initShiftRegister() {
  pinMode(latchPin, OUTPUT);
  pinMode(clockPin, OUTPUT);
  pinMode(dataPin, OUTPUT);

  pinMode(EN, OUTPUT);
  digitalWrite(EN, LOW);
}

void initReadingBatteryVoltage() {
  pinMode(BATTERY_VOLT, INPUT);
}

void initTempHumiditySensor() {
  if (!sensor.begin()) {
    Serial.println("[ERROR] Did not find Si7021 sensor!");
    while (true) {}
  }
}

SensorValues readTempHumidity(void) {
  SensorValues sensorValues = { 0.0, 0.0 };

  // NOTE: Get 10 sensor values and smoothen it
  for (int count = 0; count < 10; count++) {
    sensorValues.temperature += sensor.readTemperature();
    sensorValues.humidity += sensor.readHumidity();
  }

  sensorValues.temperature /= 10;
  sensorValues.humidity /= 10;

  Serial.print("[INFO] Temperature: ");
  Serial.print(sensorValues.temperature);
  Serial.print(" C, Humidity: ");
  Serial.print(sensorValues.humidity);
  Serial.println(" RH%");

  return sensorValues;
}

void displayHumidity(float humidity) {
  int barHumidity = humidity/20;
  String sBar = "[INFO] Display Humidity in LED: " + String(barHumidity) + " LEDs";
  Serial.println(sBar);

  displayLED(pow(2, barHumidity) -1);

  delay(10000);  // display the humidity LEDs on-board for 10 seconds
}

void displayLED(int lednumber) {
  digitalWrite(latchPin, LOW);
  shiftOut(dataPin, clockPin, MSBFIRST, lednumber);
  digitalWrite(latchPin, HIGH);
}

// Sleep and Wakup functions
void goToSleep() {
  ESP.deepSleep(SLEEP_INTERVAL_DURATION, WAKE_RF_DISABLED);
}

// WiFi functions
void eraseWiFiCredentials() {
  WiFi.disconnect(true);
  ESP.eraseConfig();
  willEraseWiFiCredentials = false;
}

void enableWiFi() {
  WiFi.forceSleepWake();  // wakeup WiFi modem
  delay(1);
}

void disableWiFi() {
  WiFi.mode(WIFI_OFF);
  WiFi.forceSleepBegin();
  delay(1);
}

bool connectToWiFi(bool useCredsFromFlash) {
  if (useCredsFromFlash) {
    WiFi.begin();
  } else {
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);
  }

  int count = 0;
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);

    if (++count % 10 == 0) {
      debugPrintln(".");
    } else {
      debugPrint(".");
    }

    if (count >= MAX_WIFI_RECONNECT_INTERVAL) {
      if (count % 10 != 0) {
        debugPrintln("");
      }
      return false;
    }
  }

  if (count % 10 != 0) {
    debugPrintln("");
  }

  WiFi.setAutoConnect(true);
  WiFi.setAutoReconnect(true);
  return true;
}

void initAccessPoint() {
  WiFi.mode(WIFI_AP);
  WiFi.softAP(createAPName().c_str(), WiFiAPPSK);
}

String createAPName() {
  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();
  return AP_NamePrefix + macID;
}

void startServer() {
  server.on("/", handleRoot);

  const char * headerkeys[] = {"User-Agent", "Cookie"};
  size_t headerkeyssize = sizeof(headerkeys)/sizeof(char*);

  server.collectHeaders(headerkeys, headerkeyssize);
  server.begin();

  isAPWebServerRunning = true;
}

void handleRoot() {
  if (server.hasArg("ssid") && server.hasArg("password") && server.hasArg("key") && server.hasArg("event")) {
    String receivedSSID = server.arg("ssid");

    debugPrintln("[INFO] WiFi SSID received: " + receivedSSID);
    receivedSSID.toCharArray(ssid, 50);

    debugPrintln("[INFO] WiFi password received");
    server.arg("password").toCharArray(password, 50);
    // Serial.println(server.arg("password"));

    debugPrintln("[INFO] IFTTT key received!");
    writeKey(server.arg("key"));
    // Serial.println(server.arg("key"));

    debugPrintln("[INFO] IFTTT event name received!");
    writeEvent(server.arg("event"));
    // Serial.println(server.arg("event"));

    returnSuccessPage();
    delay(1000);

    bool hasConectedToWifi = connectToWiFi(false);

    if (!hasConectedToWifi) {
      Serial.println("[ERROR] Cannot connect to WiFi after AP mode!");
      server.sendHeader("Location", "/");
      server.sendHeader("Cache-Control", "no-cache");
      server.sendHeader("Set-Cookie", "ESPSESSIONID=1");
      server.send(301);
      return;
    }

    debugPrintln("[INFO] Connected to WiFi after AP mode!");

    isAPWebServerRunning = false;
    return;
  }

  returnConfigPage();
}

void returnConfigPage() {
  server.send(200, "text/html", content);
}

void returnSuccessPage() {
  String content = "<html><body><h1>Received!</h1></body></html>";
  server.send(301, "text/html", content);
}

// Cloud
void sendToIFTTT(SensorValues sensorValues, float batteryVoltage) {
  Serial.println("[INFO] Sending IFTTT notification...");

  WiFiClientSecure client;
  client.setFingerprint(fingerprint);
  // Serial.println("[INFO] Setting fingerprint...");

  if (!client.connect(host, httpsPort)) {
    Serial.println("[ERROR] Connection failed");
    return;
  }

  Serial.println("[INFO] Client connected");

  String url = "/trigger/" + readEvent() + "/with/key/" + readKey();
  // Serial.print("URL: ");
  // Serial.println(url);

  char data[34];
  // TODO: Never use sprintf. Use snprintf instead.  [runtime/printf]
  sprintf(data, "value1=%03d&value2=%03d&value3=%2.1f", roundToInt(sensorValues.temperature), roundToInt(sensorValues.humidity), batteryVoltage);

  Serial.print("[INFO] Data sent: ");
  Serial.println(data);
  Serial.print("[INFO] Data size: ");
  Serial.println(sizeof(data));

  client.println(String("POST ") + url + " HTTP/1.1");
  client.println(String("Host: ") + host);
  client.println(String("Content-Type: application/x-www-form-urlencoded"));
  client.print(String("Content-Length: "));
  client.println(sizeof(data));
  client.println();
  client.println(data);

  Serial.println("[INFO] Client posted");

  unsigned long timeout = millis();
  while (client.available() == 0) {
    if (millis() - timeout > 20000) {
      Serial.println("[ERROR] Client Timeout!");
      client.stop();
      return;
    }
  }

  Serial.println("[INFO] Reply from client:");
  while (client.available()) {
    String line = client.readStringUntil('\r');
    Serial.print(line);
  }

  client.stop();
  return;
}

// Others
int roundToInt(float value) {
  return (int)round(value);
}

Makefile

BOARD?=esp8266com:esp8266:d1_mini
PORT?=/dev/cu.wchusbserial1410

.PHONY: default lint all flash clean

default: lint all flash clean

lint:
	cpplint --extensions=ino --filter=-legal/copyright,-readability/todo,-readability/casting,-runtime/int,-whitespace/line_length,-runtime/printf *.ino

all:
	arduino-cli compile --fqbn $(BOARD) ./

flash:
	arduino-cli upload -p $(PORT) --fqbn $(BOARD) ./

clean:
	rm -f .*.bin
	rm -f .*.elf

Serial console

Firmware serial console

Test

Download test code
#include <ESP8266WiFi.h>
#include "Adafruit_Si7021.h"

#define EN 2  // GPIO02 on ESP-01 module, D4 on nodeMCU WeMos
#define USERBUTTON 12  // GPIO012 on ESP or D6 on WeMos

int dataPin = 13;  // pin D7 `GPIO13` on NodeMCU boards
int clockPin = 14;  // pin D5 `GPIO14` on NodeMCU boards
int latchPin = 15;  // pin D8 `GPIO15` on NodeMCU boards

Adafruit_Si7021 sensor = Adafruit_Si7021();

void setup() {
  Serial.begin(115200);
  Serial.setTimeout(2000);
  while (!Serial) { }

  int userButtonValue = digitalRead(USERBUTTON);

  Serial.println("\n\nTest 1/10: It expects to wake up periodically");

  if (userButtonValue == 0) {
    Serial.println("Test 10/10: It expects to wake up on user long press");
  }

  initShiftRegister();
  initTempHumiditySensor();

  // Test 5 LEDs and shift register
  displayLED(0);
  delay(500);

  Serial.println("Test 2/10: It expects to make LED 1 ON");
  displayLED(1);
  delay(500);

  Serial.println("Test 3/10: It expects to make LED 1, LED 2 ON");
  displayLED(3);
  delay(500);

  Serial.println("Test 4/10: It expects to make LED 1, LED 2, LED 3 ON");
  displayLED(7);
  delay(500);

  Serial.println("Test 5/10: It expects to make LED 1, LED 2, LED 3, LED 4 ON");
  displayLED(15);
  delay(500);

  Serial.println("Test 6/10: It expects to make all 5 LEDs ON");
  displayLED(31);
  delay(500);
}

void loop() {
  displayTempHumidity();
  disableLEDs();

  Serial.println("Test 9/10: It expects to go to sleep for 10 seconds");
  Serial.println("[INFO] Press user button to test wakeup on button press!");
  Serial.println("");
  Serial.println("");
  ESP.deepSleep(10e6);
}

void initShiftRegister() {
  pinMode(latchPin, OUTPUT);
  pinMode(clockPin, OUTPUT);
  pinMode(dataPin, OUTPUT);

  pinMode(EN, OUTPUT);
  digitalWrite(EN, LOW);
}

void initTempHumiditySensor() {
  if (!sensor.begin()) {
    Serial.println("Did not find Si7021 sensor!");
    while (true) {}
  }
}

void displayLED(int lednumber) {
  digitalWrite(latchPin, LOW);
  shiftOut(dataPin, clockPin, MSBFIRST, lednumber);
  digitalWrite(latchPin, HIGH);
}

void displayTempHumidity(void) {
  Serial.print("Test 7/10: It expects to read the current temperature ");
  Serial.print(sensor.readTemperature());
  Serial.print("C and humidity ");
  Serial.print(sensor.readHumidity());
  Serial.println(" RH%");

  int barHumidity = sensor.readHumidity()/20 + 1;
  String sBar = String(barHumidity) + " LEDs";
  Serial.print("Test 8/10: It expects to display the sensor values with ");
  Serial.println(sBar);

  displayLED(pow(2, barHumidity) -1);
  delay(5000);
}

void disableLEDs(void) {
  digitalWrite(EN, LOW);
  displayLED(0);
}

Makefile

.PHONY: lint compile upload clean

lint:
	cpplint --extensions=ino --filter=-legal/copyright *.ino

compile:
	arduino-cli compile --fqbn esp8266com:esp8266:d1_mini ./

upload:
	arduino-cli upload -p /dev/cu.wchusbserial1410 --fqbn esp8266com:esp8266:d1_mini ./

clean:
	rm -f .*.bin
	rm -f .*.elf

flash: lint compile upload clean

Serial console

Firmware serial console

Web config page

This web config page is displayed when the device is in the configuration mode to setup WiFi and IFTTT credentials. It uses Bulma CSS with UnCSS to extract only the used CSS styles so that no CDN access is required in the Access Point mode.

Download code View demo
<html>
<head>
  <title>Configure [email protected]</title>
  <style>.button { -webkit-touch-callout:none; -webkit-user-select:none; -moz-user-select:none; -ms-user-select:none; user-select:none } .button,.input { -moz-appearance:none; -webkit-appearance:none; align-items:center; border:1px solid transparent; border-radius:4px; box-shadow:none; display:inline-flex; font-size:1rem; height:2.25em; justify-content:flex-start; line-height:1.5; padding-bottom:calc(.375em - 1px); padding-left:calc(.625em - 1px); padding-right:calc(.625em - 1px); padding-top:calc(.375em - 1px); position:relative; vertical-align:top } .button:active, .button:focus, .input:active, .input:focus { outline:0 } body,h1,html,p { margin:0; padding:0} h1 {font-size:100%; font-weight:400} input {margin:0} html {box-sizing:border-box} *,::after,::before {box-sizing:inherit} html{ background-color:#fff; font-size:16px; -moz-osx-font-smoothing:grayscale; -webkit-font-smoothing:antialiased; min-width:300px; overflow-x:hidden; overflow-y:scroll; text-rendering:optimizeLegibility; -webkit-text-size-adjust:100%; -moz-text-size-adjust:100%; -ms-text-size-adjust:100%; text-size-adjust:100%} section {display:block} body,input {font-family:BlinkMacSystemFont,-apple-system,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Fira Sans','Droid Sans','Helvetica Neue',Helvetica,Arial,sans-serif} body {color:#4a4a4a; font-size:1em; font-weight:400; line-height:1.5} a {color:#3273dc; cursor:pointer; text-decoration:none} a:hover {color:#363636}.button {background-color:#fff; border-color:#dbdbdb; border-width:1px; color:#363636; cursor:pointer; justify-content:center; padding-bottom:calc(.375em - 1px); padding-left:.75em; padding-right:.75em; padding-top:calc(.375em - 1px); text-align:center; white-space:nowrap}.button:hover {border-color:#b5b5b5; color:#363636}.button:focus {border-color:#3273dc; color:#363636}.button.is-focused:not(:active),.button:focus:not(:active) {box-shadow:0 0 0 .125em rgba(50,115,220,.25)}.button:active {border-color:#4a4a4a; color:#363636} .button.is-primary {background-color:#00d1b2; border-color:transparent; color:#fff} .button.is-primary:hover {background-color:#00c4a7; border-color:transparent; color:#fff} .button.is-primary:focus {border-color:transparent; color:#fff} .button.is-primary.is-focused:not(:active), .button.is-primary:focus:not(:active) {box-shadow:0 0 0 .125em rgba(0,209,178,.25)} .button.is-primary:active {background-color:#00b89c; border-color:transparent; color:#fff} .button.is-large {font-size:1.5rem}.container {flex-grow:1; margin:0 auto; position:relative; width:auto} @media screen and (min-width:1024px) {.container {max-width:960px}} @media screen and (min-width:1216px) {.container {max-width:1152px}} @media screen and (min-width:1408px) { }.container {max-width:1344px}}.subtitle {word-break:break-word}.subtitle {color:#4a4a4a; font-size:1.25rem; font-weight:400; line-height:1.25}.subtitle.is-1 {font-size:3rem}.input {background-color:#fff; border-color:#dbdbdb; border-radius:4px; color:#363636}.input::-moz-placeholder {color:rgba(54,54,54,.3)}.input::-webkit-input-placeholder {color:rgba(54,54,54,.3)}.input:-moz-placeholder {color:rgba(54,54,54,.3)}.input:-ms-input-placeholder {color:rgba(54,54,54,.3)}.input:hover {border-color:#b5b5b5}.input:active,.input:focus {border-color:#3273dc; box-shadow:0 0 0 .125em rgba(50,115,220,.25)}.input {box-shadow:inset 0 1px 2px rgba(10,10,10,.1); max-width:100%; width:100%}.is-medium.input {font-size:1.25rem}.select.is-white:not(:hover)::after {border-color:#fff}.select.is-black:not(:hover)::after {border-color:#0a0a0a}.select.is-light:not(:hover)::after {border-color:#f5f5f5}.select.is-dark:not(:hover)::after {border-color:#363636}.select.is-primary:not(:hover)::after {border-color:#00d1b2}.select.is-link:not(:hover)::after {border-color:#3273dc}.select.is-info:not(:hover)::after {border-color:#209cee}.select.is-success:not(:hover)::after {border-color:#23d160}.select.is-warning:not(:hover)::after {border-color:#ffdd57}.select.is-danger:not(:hover)::after {border-color:#ff3860}.label {color:#363636; display:block; font-size:1rem; font-weight:700}.help {display:block; font-size:.75rem; margin-top:.25rem}.field:not(:last-child) {margin-bottom:.75rem} @media screen and (min-width:769px),print {.field.is-horizontal {display:flex}}.field-label .label {font-size:inherit} @media screen and (max-width:768px) {.field-label {margin-bottom:.5rem}} @media screen and (min-width:769px),print {.field-label {flex-basis:0; flex-grow:1; flex-shrink:0; margin-right:1.5rem; text-align:right}.field-label.is-medium {font-size:1.25rem; padding-top:.375em}} @media screen and (min-width:769px),print {.field-body {display:flex; flex-basis:0; flex-grow:5; flex-shrink:1}.field-body .field {margin-bottom:0}.field-body>.field {flex-shrink:1}.field-body>.field:not(.is-narrow) {flex-grow:1}}.control {box-sizing:border-box; clear:both; font-size:1rem; position:relative; text-align:left} @media screen and (min-width:1024px) {.navbar-link.is-active:not(:focus):not(:hover),a.navbar-item.is-active:not(:focus):not(:hover) {background-color:transparent}}.hero {align-items:stretch; display:flex; flex-direction:column; justify-content:space-between}.hero.is-light {background-color:#f5f5f5; color:#363636}.hero.is-light .subtitle {color:rgba(54,54,54,.9)}.hero.is-small .hero-body {padding-bottom:1.5rem; padding-top:1.5rem}.hero-body {flex-grow:1; flex-shrink:0; padding:3rem 1.5rem}.section {padding:3rem 1.5rem}</style>
</head>
<body>
  <section class='hero is-small is-light'>
    <div class='hero-body'>
      <div class='container'>
        <h1 class='subtitle is-1'>Configure [email protected]</h1>
      </div>
    </div>
  </section>

  <section class='section is-small'>
    <div class='container'>
      <form action='/' method='post'>

        <div class='field is-horizontal'>
          <div class='field-label is-medium'>
            <label class='label'>WiFi SSID</label>
          </div>
          <div class='field-body'>
            <div class='field'>
              <div class='control'>
                <input class='input is-medium' type='text' name='ssid' placeholder='WiFi SSID'>
              </div>
            </div>
          </div>
        </div>

        <div class='field is-horizontal'>
          <div class='field-label is-medium'>
            <label class='label'>WiFi Password</label>
          </div>
          <div class='field-body'>
            <div class='field'>
              <div class='control'>
                <input class='input is-medium' type='password' name='password' placeholder='WiFi Password'>
              </div>
            </div>
          </div>
        </div>

        <div class='field is-horizontal'>
          <div class='field-label is-medium'>
            <label class='label'>IFTTT Key</label>
          </div>
          <div class='field-body'>
            <div class='field'>
              <div class='control'>
                <input class='input is-medium' type='text' name='key' placeholder='IFTTT Key 22 Character length'>
              </div>
              <p class='help'>Check <a href='https://ifttt.com/maker_webhooks'>IFTTT Maker Webhooks</a> page.</p>
            </div>
          </div>
        </div>

        <div class='field is-horizontal'>
          <div class='field-label is-medium'>
            <label class='label'>IFTTT Event</label>
          </div>
          <div class='field-body'>
            <div class='field'>
              <div class='control'>
                <input class='input is-medium' type='text' name='event' placeholder='IFTTT Maker Event name'>
              </div>
            </div>
          </div>
        </div>

        <div class='field is-horizontal'>
          <div class='field-label'>
          </div>
          <div class='field-body'>
            <div class='field'>
              <div class='control'>
                <input type='submit' class='button is-primary is-large' name='submit' value='Submit'>
              </div>
            </div>
          </div>
        </div>
      </form>
    </div>
  </section>
</body>
</html>