diff --git a/assets/1416589372_dashboard_medium_512.png b/assets/1416589372_dashboard_medium_512.png
new file mode 100644
index 0000000000000000000000000000000000000000..b2c713a1678744fa7468a73cd84eddc66dfe20e7
Binary files /dev/null and b/assets/1416589372_dashboard_medium_512.png differ
diff --git a/data/browserconfig.xml b/data/browserconfig.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ba9981335cd01a5cddc7600a3d6bf0b75d37b28d
--- /dev/null
+++ b/data/browserconfig.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<browserconfig>
+    <msapplication>
+        <tile>
+            <square150x150logo src="img/mstile-150x150.png"/>
+            <TileColor>#da532c</TileColor>
+        </tile>
+    </msapplication>
+</browserconfig>
diff --git a/data/favicon.ico b/data/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..f240d44fe35ec981ead1b0f3f95c17f065a29b46
Binary files /dev/null and b/data/favicon.ico differ
diff --git a/data/img/android-chrome-192x192.png b/data/img/android-chrome-192x192.png
new file mode 100644
index 0000000000000000000000000000000000000000..5ac8b925d9e92d7d34ade2c395c672eda34a16e8
Binary files /dev/null and b/data/img/android-chrome-192x192.png differ
diff --git a/data/img/android-chrome-512x512.png b/data/img/android-chrome-512x512.png
new file mode 100644
index 0000000000000000000000000000000000000000..1a3734fe2a2e26123f33691befe9c1eae3b52014
Binary files /dev/null and b/data/img/android-chrome-512x512.png differ
diff --git a/data/img/apple-touch-icon.png b/data/img/apple-touch-icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..0feb785f8e5d6316185515e2ec64588b833cc090
Binary files /dev/null and b/data/img/apple-touch-icon.png differ
diff --git a/data/img/favicon-16x16.png b/data/img/favicon-16x16.png
new file mode 100644
index 0000000000000000000000000000000000000000..79a8a4814560f97c89f1eff5d6f926ad5ea19e7b
Binary files /dev/null and b/data/img/favicon-16x16.png differ
diff --git a/data/img/favicon-32x32.png b/data/img/favicon-32x32.png
new file mode 100644
index 0000000000000000000000000000000000000000..619638bacaf10c4a16d88730bd8d1c6bfe32a319
Binary files /dev/null and b/data/img/favicon-32x32.png differ
diff --git a/data/img/mstile-150x150.png b/data/img/mstile-150x150.png
new file mode 100644
index 0000000000000000000000000000000000000000..94ea937706c85bdf9da6976f9df0f5b657780206
Binary files /dev/null and b/data/img/mstile-150x150.png differ
diff --git a/data/index.html b/data/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..bec52089e8bca0bf9afeb033cb700fbfd4d31767
--- /dev/null
+++ b/data/index.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <link rel="apple-touch-icon" sizes="180x180" href="img/apple-touch-icon.png">
+  <link rel="icon" type="image/png" sizes="32x32" href="img/favicon-32x32.png">
+  <link rel="icon" type="image/png" sizes="16x16" href="img/favicon-16x16.png">
+  <link rel="manifest" href="/site.webmanifest">
+  <meta name="msapplication-TileColor" content="#da532c">
+  <meta name="theme-color" content="#ffffff">
+  <title>Air Quality Monitor</title>
+</head>
+<body>
+dis gon b gud.
+</body>
+</html>
+
diff --git a/data/site.webmanifest b/data/site.webmanifest
new file mode 100644
index 0000000000000000000000000000000000000000..462ff017caaccc75c092914119a05036ecae0c1d
--- /dev/null
+++ b/data/site.webmanifest
@@ -0,0 +1,19 @@
+{
+    "name": "",
+    "short_name": "",
+    "icons": [
+        {
+            "src": "img/android-chrome-192x192.png",
+            "sizes": "192x192",
+            "type": "image/png"
+        },
+        {
+            "src": "img/android-chrome-512x512.png",
+            "sizes": "512x512",
+            "type": "image/png"
+        }
+    ],
+    "theme_color": "#ffffff",
+    "background_color": "#ffffff",
+    "display": "standalone"
+}
diff --git a/platformio.ini b/platformio.ini
index d379479d4938370a44a4042470feab6ab46a789c..4a6cf5fd163c43526d5b7e736a97a839cc442072 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -8,19 +8,26 @@
 ; Please visit documentation for the other options and examples
 ; https://docs.platformio.org/page/projectconf.html
 
-[env:esp32dev]
-platform = espressif32
-board = esp32dev
+[env:d1_mini]
+platform = espressif8266
+board = d1_mini
 framework = arduino
+upload_port = COM6
 monitor_speed = 115200
-monitor_port = COM4
+monitor_port = COM6
+board_build.filesystem = littlefs
+board_build.ldscript = eagle.flash.4m1m.ld
 lib_deps =
-    avaldebe/PMSerial @ ^1.1.1
-    ESP Async WebServer
+    ESP8266WiFi
+    ESP8266WebServer
+    ESP8266mDNS
     ArduinoOTA
+    plerup/EspSoftwareSerial @ ^6.15.1
+    avaldebe/PMSerial @ ^1.1.1
+    boschsensortec/BSEC Software Library @ ^1.6.1480
     knolleary/PubSubClient @ ^2.8
 
-[env:esp32dev_ota]
-extends = env:esp32dev
+[env:d1_mini_ota]
+extends = env:d1_mini
 upload_port = airbox.local
 upload_protocol = espota
diff --git a/src/main.cpp b/src/main.cpp
index 735e8183a044c2c861cfd00beb6c3b8fa022a512..71889a8e4d3ddcee6424ada7aaf7b974e2d1594e 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,30 +1,32 @@
 #include "config.h"
 #include <Arduino.h>
-#include <PMserial.h>
-#include <WiFi.h>
-#include <Wire.h>
-#include <ESPmDNS.h>
-#include <WiFiUdp.h>
+#include <SoftwareSerial.h>
+#include <ESP8266WiFi.h>
+#include <FS.h>
+#include <LittleFS.h>
 #include <ArduinoOTA.h>
-#include <ESPAsyncWebServer.h>
+#include <ESP8266mDNS.h>
+#include <ESP8266WebServer.h>
+#include <Wire.h>
+#include <PMserial.h>
+#include <bsec.h>
 #include <PubSubClient.h>
 
-//
-// Configuration
-//
+#define PMS_TX D3
+#define PMS_RX D4
 
-SerialPM pms(PMS5003, Serial2);
-IPAddress mqttServer;
+SoftwareSerial swSerial;
 WiFiClient wifiClient;
+MDNSResponder mdns;
+ESP8266WebServer server(80);
+SerialPM pms(PMS5003, PMS_RX, PMS_TX);
+Bsec iaqSensor;
 PubSubClient mqttClient(wifiClient);
-AsyncWebServer server(80);
+IPAddress mqttServer;
 
 long checkMillis = 0;
 long checkInterval = 5000; // 5s
 
-#define PMS_RX 16
-#define PMS_TX 17
-
 void logLine(String line, bool newline = true)
 {
   Serial.print(line);
@@ -37,6 +39,7 @@ void logLine(String line, bool newline = true)
 void setupWifi()
 {
   logLine("Connecting to '" + String(WIFI_SSID) + "' ", false);
+  WiFi.hostname(HOSTNAME);
   WiFi.begin(WIFI_SSID, WIFI_PSK);
 
   int i = 50;
@@ -64,28 +67,53 @@ void setupWifi()
   }
 }
 
-void setupWebserver()
-{
-  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
-            { request->send(200, "text/plain", "ok"); });
-  server.begin();
+String getContentType(String filename)
+{ // convert the file extension to the MIME type
+  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(".ico"))
+    return "image/x-icon";
+  else if (filename.endsWith(".gz"))
+    return "application/x-gzip";
+  return "text/plain";
 }
 
-void setupMDNS()
-{
-  if (MDNS.begin(HOSTNAME))
-  {
-    logLine("MDNS responder started as " + String(HOSTNAME));
-    MDNS.addService("http", "tcp", 80);
-  }
-  else
-  {
-    logLine("MDNS.begin failed");
+bool handleFileRead(String path)
+{ // send the right file to the client (if it exists)
+  Serial.println("handleFileRead: " + path);
+  if (path.endsWith("/"))
+    path += "index.html";                    // If a folder is requested, send the index file
+  String contentType = getContentType(path); // Get the MIME type
+  String pathWithGz = path + ".gz";
+  if (LittleFS.exists(pathWithGz) || LittleFS.exists(path))
+  {                                                     // If the file exists, either as a compressed archive, or normal
+    if (LittleFS.exists(pathWithGz))                    // If there's a compressed version available
+      path += ".gz";                                    // Use the compressed version
+    File file = LittleFS.open(path, "r");               // Open the file
+    size_t sent = server.streamFile(file, contentType); // Send it to the client
+    file.close();                                       // Close the file again
+    Serial.printf("Sent file: %s (%d)\r\n", path.c_str(), sent);
+    return true;
   }
+  Serial.println(String("\tFile Not Found: ") + path);
+  return false; // If the file doesn't exist, return false
 }
 
-void messageReceived(String &topic, String &payload)
+void setupWebserver()
 {
+  LittleFS.begin();
+  server.onNotFound([]() {                              // If the client requests any URI
+    if (!handleFileRead(server.uri()))                  // send it if it exists
+      server.send(404, "text/plain", "404: Not Found"); // otherwise, respond with a 404 (Not Found) error
+  });
+  server.begin();
+}
+
+void messageReceived(String &topic, String &payload) {
   Serial.println("incoming: " + topic + " - " + payload);
 }
 
@@ -122,75 +150,113 @@ void setupMQTT()
 void setupOTA()
 {
   ArduinoOTA.setHostname(HOSTNAME);
-  ArduinoOTA
-      .onStart([]()
-               {
-                 String type;
-                 if (ArduinoOTA.getCommand() == U_FLASH)
-                   type = "sketch";
-                 else // U_SPIFFS
-                   type = "filesystem";
-
-                 // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
-                 Serial.println("Start updating " + type);
-               })
-      .onEnd([]()
-             { Serial.println("\nEnd"); })
-      .onProgress([](unsigned int progress, unsigned int total)
-                  { Serial.printf("Progress: %u%%\r", (progress / (total / 100))); })
-      .onError([](ota_error_t error)
-               {
-                 Serial.printf("Error[%u]: ", error);
-                 if (error == OTA_AUTH_ERROR)
-                   Serial.println("Auth Failed");
-                 else if (error == OTA_BEGIN_ERROR)
-                   Serial.println("Begin Failed");
-                 else if (error == OTA_CONNECT_ERROR)
-                   Serial.println("Connect Failed");
-                 else if (error == OTA_RECEIVE_ERROR)
-                   Serial.println("Receive Failed");
-                 else if (error == OTA_END_ERROR)
-                   Serial.println("End Failed");
-               });
-
+  ArduinoOTA.onStart([]() {
+    String type;
+    if (ArduinoOTA.getCommand() == U_FLASH)
+    {
+      type = "sketch";
+    }
+    else
+    { // U_SPIFFS
+      type = "filesystem";
+      LittleFS.end();
+    }
+    Serial.println("Start updating " + type);
+  });
+  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
+    Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
+  });
+  ArduinoOTA.onEnd([]() {
+    Serial.println("\nEnd");
+  });
+  ArduinoOTA.onError([](ota_error_t error) {
+    Serial.printf("Error[%u]: ", error);
+    if (error == OTA_AUTH_ERROR)
+      Serial.println("Auth Failed");
+    else if (error == OTA_BEGIN_ERROR)
+      Serial.println("Begin Failed");
+    else if (error == OTA_CONNECT_ERROR)
+      Serial.println("Connect Failed");
+    else if (error == OTA_RECEIVE_ERROR)
+      Serial.println("Receive Failed");
+    else if (error == OTA_END_ERROR)
+      Serial.println("End Failed");
+  });
   ArduinoOTA.begin();
 }
 
-void publishMetric(uint16_t data[])
-{
-  String stringPmData = "\"pm1\": " + String(data[0]) + ", \"pm2.5\": " + String(data[1]) + ", \"pm10\": " + String(data[2]);
-  String stringNcData = "\"nc0.3\": " + String(data[3]) + ", \"nc0.5\": " + String(data[4]) + ", \"nc1\": " + String(data[5]) + ", \"nc2.5\": " + String(data[6]) + ", \"nc5\": " + String(data[7]) + ", \"nc10\": " + String(data[8]);
-  String payloadString = "{\"name\": \"air\", \"node\": \"" + String(HOSTNAME) + "\", " + stringPmData + ", " + stringNcData + "}";
-  //logLine(payloadString);
-  int len_payload = payloadString.length() + 1;
-  char payload[len_payload];
-  payloadString.toCharArray(payload, len_payload);
-  mqttClient.publish(MQTT_TOPIC, payload);
-}
-
 void setupPms()
 {
   Serial.println("Initializing PMS, wait 30 seconds for stable readings...");
-  Serial2.begin(9600, SERIAL_8N1, PMS_RX, PMS_TX);
+  swSerial.begin(9600, SWSERIAL_8N1, PMS_RX, PMS_TX);
   pms.init();
 }
 
+void checkIaqSensorStatus(void)
+{
+  if (iaqSensor.status != BSEC_OK)
+  {
+    if (iaqSensor.status < BSEC_OK)
+    {
+      logLine("BSEC error code : " + String(iaqSensor.status));
+    }
+    else
+    {
+      logLine("BSEC warning code : " + String(iaqSensor.status));
+    }
+  }
+
+  if (iaqSensor.bme680Status != BME680_OK)
+  {
+    if (iaqSensor.bme680Status < BME680_OK)
+    {
+      logLine("BME680 error code : " + String(iaqSensor.bme680Status));
+    }
+    else
+    {
+      logLine("BME680 warning code : " + String(iaqSensor.bme680Status));
+    }
+  }
+}
+
+void setupBme()
+{
+  iaqSensor.begin(BME680_I2C_ADDR_PRIMARY, Wire);
+  logLine("\nBSEC library version " + String(iaqSensor.version.major) + "." + String(iaqSensor.version.minor) + "." + String(iaqSensor.version.major_bugfix) + "." + String(iaqSensor.version.minor_bugfix));
+  checkIaqSensorStatus();
+  bsec_virtual_sensor_t sensorList[10] = {
+      BSEC_OUTPUT_RAW_TEMPERATURE,
+      BSEC_OUTPUT_RAW_PRESSURE,
+      BSEC_OUTPUT_RAW_HUMIDITY,
+      BSEC_OUTPUT_RAW_GAS,
+      BSEC_OUTPUT_IAQ,
+      BSEC_OUTPUT_STATIC_IAQ,
+      BSEC_OUTPUT_CO2_EQUIVALENT,
+      BSEC_OUTPUT_BREATH_VOC_EQUIVALENT,
+      BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE,
+      BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY,
+  };
+
+  iaqSensor.updateSubscription(sensorList, 10, BSEC_SAMPLE_RATE_LP);
+  checkIaqSensorStatus();
+}
+
 void setup()
 {
+  Wire.begin();
   Serial.begin(115200);
   delay(10);
   Serial.println();
   setupPms();
+  setupBme();
   setupWifi();
   setupOTA();
   setupWebserver();
-  setupMDNS();
   setupMQTT();
 }
 
-void publishPms()
+String readPms()
 {
-
   pms.read();
   if (pms)
   { // successfull read
@@ -203,7 +269,7 @@ void publishPms()
     Serial.printf("NC >=2.5µm: %5d #/100cm³\r\n", pms.n2p5);
     Serial.printf("NC >=  5µm: %5d #/100cm³\r\n", pms.n5p0);
     Serial.printf("NC >= 10µm: %5d #/100cm³\r\n", pms.n10p0);
-    publishMetric(pms.data);
+    logLine("-----");
   }
   else
   {
@@ -237,12 +303,68 @@ void publishPms()
       break;
     }
   }
-  Serial.println("...............................................");
+  String pmData = "pm1=" + String(pms.data[0]) + ",pm2.5=" + String(pms.data[1]) + ",pm10=" + String(pms.data[2]);
+  String ncData = "nc0.3=" + String(pms.data[3]) + ",nc0.5=" + String(pms.data[4]) + ",nc1=" + String(pms.data[5]) + ",nc2.5=" + String(pms.data[6]) + ",nc5=" + String(pms.data[7]) + ",nc10=" + String(pms.data[8]);
+  return pmData + "," + ncData;
+}
+
+String readBme()
+{
+  if (iaqSensor.run())
+  { // If new data is available
+    Serial.printf("Temperature  : %f °C\r\n", iaqSensor.temperature);
+    Serial.printf("Humidity     : %f %%\r\n", iaqSensor.humidity);
+    Serial.printf("Pressure     : %f hPa\r\n", iaqSensor.pressure);
+    Serial.printf("IAQ          : %f\r\n", iaqSensor.iaq);
+    Serial.printf("IAQ Accuracy : %d\r\n", iaqSensor.iaqAccuracy);
+    Serial.printf("Static IAQ   : %f\r\n", iaqSensor.staticIaq);
+    Serial.printf("Stat IAQ Acc : %d\r\n", iaqSensor.staticIaqAccuracy);
+    Serial.printf("CO2 Equiv    : %f\r\n", iaqSensor.co2Equivalent);
+    Serial.printf("CO2 Accuracy : %d\r\n", iaqSensor.co2Accuracy);
+    Serial.printf("bVOC Equiv   : %f\r\n", iaqSensor.breathVocEquivalent);
+    Serial.printf("bVOC Accuracy: %d\r\n", iaqSensor.breathVocAccuracy);
+    Serial.printf("Gas Percent  : %f %%\r\n", iaqSensor.gasPercentage);
+    Serial.printf("Gas Per Accur: %d\r\n", iaqSensor.gasPercentageAcccuracy);
+    Serial.printf("Raw Temp     : %f °C\r\n", iaqSensor.rawTemperature);
+    Serial.printf("Raw Rel Humid: %f %%\r\n", iaqSensor.rawHumidity);
+    Serial.printf("Gas Resist   : %f Ohm\r\n", iaqSensor.gasResistance);
+  }
+  else
+  {
+    checkIaqSensorStatus();
+  }
+  return "temperature=" + String(iaqSensor.temperature) + ",humidity=" + String(iaqSensor.humidity) + ",pressure=" + String(iaqSensor.pressure) +
+         ",iaq=" + String(iaqSensor.iaq) + ",iaq_acc=" + String(iaqSensor.iaqAccuracy) +
+         ",s_iaq=" + String(iaqSensor.staticIaq) + ",s_iaq_acc=" + String(iaqSensor.staticIaqAccuracy) +
+         ",eco2=" + String(iaqSensor.co2Equivalent) + ",eco2_acc=" + String(iaqSensor.co2Accuracy) +
+         ",bvoc=" + String(iaqSensor.breathVocEquivalent) + ",bvoc_acc=" + String(iaqSensor.breathVocAccuracy);
+}
+
+void publishMetric(String metricValues)
+{
+  String payloadString = "air,node=" + String(HOSTNAME) + " " + metricValues;
+  //logLine(payloadString);
+  int len_payload = payloadString.length() + 1;
+  char payload[len_payload];
+  payloadString.toCharArray(payload, len_payload);
+  mqttClient.publish(MQTT_TOPIC, payload);
+}
+
+void getSensorData()
+{
+
+  String pmsData = readPms();
+  String bmeData = readBme();
+  String metricValues = pmsData + "," + bmeData;
+  publishMetric(metricValues);
+  logLine("#####");
 }
 
 void loop()
 {
   ArduinoOTA.handle();
+  server.handleClient();
+  mdns.update();
 
   unsigned long currentMillis = millis();
   if (currentMillis - checkMillis > checkInterval)
@@ -264,7 +386,6 @@ void loop()
   if (millis() - updateTimer >= 5000)
   {
     updateTimer = millis();
-    publishPms();
+    getSensorData();
   }
-  
 }