├── .gitignore ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── images ├── Graph-Home-Assistant.png ├── Overview-Screenshot.png ├── WiFi-setup-done-Screenshot.png └── Wiring.png ├── platformio.ini └── src └── vindriktning.ino /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/.browse.c_cpp.db* 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/ipch 6 | html 7 | .vscode/settings.json 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "platformio.platformio-ide" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 grmcdorman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # esp8266_vindriktning 2 |

ESP8266 D1 Mini Ikea Vindriktning Air Quality Sensor with SHT31-D Temperature/Humidity Sensor

3 | 4 | This repository contains an ESP8266 sketch for an Ikea Vindriktning air quality sensor, with an SHT31-D temperature and humidity sensor attached. It uses my [esp8266-device-framework library](https://grmcdorman.github.io/esp8266_device_framework) which in turn uses my [esp8266_web_settings](https://grmcdorman.github.io/esp8266_web_settings) library. 5 | 6 | The code is identical to the example in the device framework library. 7 | 8 | This is derived from https://github.com/Hypfer/esp8266-vindriktning-particle-sensor. 9 | 10 | | | | 11 | |-----------------------|------------------------------------| 12 | | **General warning** | If you reset or otherwise disconnect the server while the web page is running, when it comes back up it may overload your device with queued XHR requests, since there is a request queued every 5 seconds. If the device isn't responsive, close your browser tabs, wait for the device to catch up, and try again. | 13 | | | | 14 | 15 |

Wiring

16 | As detailed at Hypfer's repository, you need to open the Vindriktning and make the following connections to your ESP8266 D1 Mini: 17 | 18 | 19 | | Vindriktning board | D1 Mini Board| 20 | |--------------------|--------------| 21 | | +5 V | 5V | 22 | | GND | G or GND | 23 | | REST (testpoint) | D2 | 24 | | | | 25 | 26 | If you're adding the SHT31-D, make the following connections from it to the D1 mini: 27 | | SHT31D | D1 Mini Board | 28 | |--------|---------------| 29 | | VIN | 3.3V | 30 | | GND | G or GND | 31 | | SCL | D6 | 32 | | SDA | D5 | 33 | | | | 34 | 35 | You should be able to use different pins for the Vindriktning data, and the SHT31 SCL and SDA, if you need; any of D1, D2, D3, D5, D6 and D7 can be used (and the firmware allows you to select different pins on the web configuration page). Of course, you can't connect two data lines to the same pin, though. 36 | 37 | Visually, including the SHT31-D (see Hypfer's repository for images of the Vindriktning): 38 | 39 | ![Wiring](images/Wiring.png "Wiring") 40 | 41 |

Other Considerations

42 | 43 | | | | 44 | |-----------------------|------------------------------------| 45 | | You don't have SHT31-D | No problem. The code will detect that the sensor is not present, and disable it on startup. However, if you don't want even that overhead remove the `sht31_sensor` variable from the two places it occurs in the sketch. 46 | | You don't have a Vindriktning | In this case, you probably want to at least disable the sensor in the configuration page. Again, of course, you can remove the `vindriktning_air_quality` variable from the sketch. | 47 | | I don't like the password on every save | That's an outstanding improvement item for the [esp8266_web_settings](https://grmcdorman.github.io/esp8266_web_settings) library. Or you can just tell your browser to save the credentials. | 48 | | I don't want a password | Remove the calls to `webServer.set_credentials` in the code. | 49 | | The password is easy to guess | That's for a future enhancement as well. Ideally, the password would be something _you_ provide. The main issue is making sure you don't have a typo in the password field; at the moment the esp8266_web_settings doesn't have any way of ensuring this. | 50 | | | | 51 | 52 |

Configuration

53 | 54 | | | | 55 | |-----------------------|------------------------------------| 56 | | **NOTE** | When connected to an access point, the code is configured to ask for a user and password every time you save, reboot, reset to factory defaults, or upload firmware. The user is **admin**, and the password is always **VINDRIKTNING-**_lower-case-hex-string_, e.g. **VINDRIKTNING-f2b10e**. The hex string is the chip ID; this is shown in the System Details as the Device Chip ID. You can disable this by commenting out all calls to `set_credentials`, or you can change the `identifier` string passed to that call. | 57 | | | | 58 | 59 | When you first flash the firmware, the ESP8266 will start up with a Soft AP named **VINDRIKTNING-**_hex-string_; connect to this. You should get a "sign in prompt"; if not, navigate to http://192.168.4.1. Either way, when you get there select the WiFi tab at the top and fill in your own WiFi access point ID and password (you'll need to check the box next to the password input to enable it). In the unlikely event that your WiFi access point _doesn't_ have a password, leave the password box empty. 60 | 61 | You should also probably enter an appropriate name for your ESP8266 in the **Hostname** field. 62 | 63 | When you're done, it should look something like this: 64 | 65 | ![WiFiSetup](images/WiFi-setup-done-Screenshot.png) 66 | 67 | You can also check the other tabs at this time; if everything's wired up correctly you should see that the Vindriktning and the SHT31-D are both collecting data. 68 | 69 | At this point, you can either click on **Save** or move to the **MQTT** tab. If you click on **Save**, the ESP8266 will close the Soft AP and will attempt to connect to your WiFi. If this fails, after about a minute it will re-open the Soft AP. 70 | 71 | To configure MQTT, switch to that tab, and enter at least your MQTT host name or IP address, and the MQTT user name and password if required. Most of the other values don't need to be changed, unless you want a different topic prefix or client ID for this device. 72 | 73 | Remember to click **Save** when you've finished entering your MQTT configuration. 74 | 75 | Finally, once everything's set up to your satisfaction, click on **Reboot** to reboot the device with fresh settings. Once it reboots, it should start publishing to your MQTT server. 76 | 77 |

Calibration

78 | If you have a thermometer and/or a hydrometer, I suggest you put them next to the Vindriktning for a while so you can compare readings with the SHT31-D. I have found that the SHT31-D reads around 2 degrees high, possibly because of heat generated inside the Vindriktning case by the Vindriktning electronics and the added ESP8266. The humidity is also affected by the temperature offset; in my case it seems be about 10% R.H. lower than my Nest Thermostat when the Vindriktning is next to it. 79 | 80 | You can also try mounting the SHT31-D outside the case by cutting a hole for the connection wires. 81 | 82 |

Screenshots

83 | 84 | ![Web settings page with overview](images/Overview-Screenshot.png "Web settings page") 85 | 86 | In Home Assistant using the **mini-graph-card** from HACS: 87 | 88 | ![Home Assistant](images/Graph-Home-Assistant.png "Home Assistant") 89 | -------------------------------------------------------------------------------- /images/Graph-Home-Assistant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grmcdorman/esp8266_vindriktning/1f705f5b05aa488473b0223adc9afee647770b5f/images/Graph-Home-Assistant.png -------------------------------------------------------------------------------- /images/Overview-Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grmcdorman/esp8266_vindriktning/1f705f5b05aa488473b0223adc9afee647770b5f/images/Overview-Screenshot.png -------------------------------------------------------------------------------- /images/WiFi-setup-done-Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grmcdorman/esp8266_vindriktning/1f705f5b05aa488473b0223adc9afee647770b5f/images/WiFi-setup-done-Screenshot.png -------------------------------------------------------------------------------- /images/Wiring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grmcdorman/esp8266_vindriktning/1f705f5b05aa488473b0223adc9afee647770b5f/images/Wiring.png -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [env:d1_mini] 12 | platform = espressif8266 13 | board = d1_mini 14 | framework = arduino 15 | monitor_speed = 115200 16 | debug_port = COM3 17 | monitor_filters = esp8266_exception_decoder 18 | lib_deps = 19 | bblanchon/ArduinoJson @ ^6.18.3 20 | knolleary/PubSubClient @ ^2.8 21 | robtillaart/SHT31 22 | https://github.com/grmcdorman/esp8266_device_framework.git 23 | # I suggest you use the ESP Home Async Web Server instead of the me-no-dev 24 | # version; the ESP Home version is significantly smaller and seems 25 | # to perform better for me. 26 | esphome/ESPAsyncWebServer-esphome@^2.1.0 27 | -------------------------------------------------------------------------------- /src/vindriktning.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | #include // Required by the ESP compiler framework. 9 | 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | 21 | // Global constant strings. 22 | static const char firmware_name[] PROGMEM = "esp8266-vindriktning-particle-sensor"; 23 | static const char identifier_prefix[] PROGMEM = "VINDRIKTNING-"; 24 | static const char manufacturer[] PROGMEM = "grmcdorman"; 25 | static const char model[] PROGMEM = "Vindriktning with SHT31"; 26 | static const char software_version[] PROGMEM = "1.0.0"; 27 | 28 | // The default identifier string. 29 | static char identifier[sizeof ("VINDRIKTNING-") + 12]; 30 | 31 | // Our config file load/save and the web settings server. 32 | static grmcdorman::device::ConfigFile config; 33 | static grmcdorman::WebSettings webServer; 34 | 35 | // Device declarations. Order is not important. 36 | static ::grmcdorman::device::InfoDisplay info_display; 37 | static ::grmcdorman::device::SystemDetailsDisplay system_details_display; 38 | static ::grmcdorman::device::WifiDisplay wifi_display; 39 | static ::grmcdorman::device::WifiSetup wifi_setup; 40 | static ::grmcdorman::device::Sht31Sensor sht31_sensor; 41 | static ::grmcdorman::device::VindriktningAirQuality vindriktning_air_quality; 42 | 43 | // This uses the default WiFiClient for communications. 44 | static ::grmcdorman::device::MqttPublisher mqtt_publisher(FPSTR(manufacturer), FPSTR(model), FPSTR(software_version)); 45 | 46 | 47 | // Device list. Order _is_ important; this is the order they're presented on the web page. 48 | static std::vector devices 49 | { 50 | &info_display, 51 | &system_details_display, 52 | &wifi_display, 53 | &wifi_setup, 54 | &sht31_sensor, 55 | &vindriktning_air_quality, 56 | &mqtt_publisher 57 | }; 58 | 59 | // State. 60 | static bool set_save_credentials = false; //!< Set to true when credentials for save/restart etc. have been configured. 61 | static bool factory_reset_next_loop = false; //!< Set to true when a factory reset has been requested. 62 | static bool restart_next_loop = false; //!< Set to true when a simple reset has been requested. 63 | static uint32_t restart_reset_when = 0; //!< The time the factory reset/reset was requested. 64 | static constexpr uint32_t restart_reset_delay = 500; //!< How long after the request for factory reset/reset to actually perform the function 65 | 66 | // Forward declarations for the three web_settings callbacks. 67 | 68 | static void on_factory_reset(::grmcdorman::WebSettings &); 69 | static void on_restart(::grmcdorman::WebSettings &); 70 | static void on_save(::grmcdorman::WebSettings &); 71 | 72 | void setup() { 73 | Serial.begin(115200); 74 | Serial.println(); 75 | Serial.print(firmware_name); 76 | Serial.println(" is starting"); 77 | 78 | strcpy_P(identifier, identifier_prefix); 79 | itoa(ESP.getChipId(), identifier + strlen(identifier), 16); 80 | Serial.print("My default identifier is "); 81 | Serial.println(identifier); 82 | 83 | // Set SHT31-D. Vindriktning and MQTT to enabled by default. 84 | // These are disabled by default in the device code. 85 | sht31_sensor.set_enabled(true); 86 | vindriktning_air_quality.set_enabled(true); 87 | mqtt_publisher.set_enabled(true); 88 | 89 | // Set some defaults. 90 | // This is commented out; the recommendation is you leave the 91 | // device settings as-is and set things up through the web interface 92 | // on the soft AP. 93 | 94 | // wifi_setup.set("ssid", "my access point"); 95 | // Note that this will *not* be shown in the web page UI. 96 | // wifi_setup.set("password", "my password"); 97 | 98 | // Device index # 4 is the SHT31-D. Set the default sda to D2, scl to D3. 99 | // sht31_sensor.set("sda", "D2"); 100 | // sht31_sensor.set("scl", "D3"); 101 | 102 | for (auto &device: devices) 103 | { 104 | device->set_system_identifiers(FPSTR(firmware_name), identifier); 105 | device->set_defaults(); 106 | } 107 | 108 | config.load(devices); 109 | 110 | // If you wanted to override settings, you could call device 'set' methods here. 111 | 112 | // Print some device settings. 113 | Serial.print("WiFi SSID is "); 114 | Serial.println(devices[3]->get("ssid")); 115 | Serial.print("SHT31-D SDA is on pin "); 116 | Serial.println(devices[4]->get("sda")); 117 | 118 | for (auto &device : devices) 119 | { 120 | device->setup(); 121 | device->set_devices(devices); 122 | webServer.add_setting_set(device->name(), device->identifier(), device->get_settings()); 123 | } 124 | 125 | if (WiFi.isConnected()) 126 | { 127 | // SoftAP capture portal clients are typically not happy about authentication. 128 | webServer.set_credentials("admin", identifier); 129 | set_save_credentials = true; 130 | setupOTA(); 131 | } 132 | 133 | webServer.setup(on_save, on_restart, on_factory_reset); 134 | 135 | Serial.print(F("IP: ")); 136 | Serial.println(WiFi.localIP().toString()); 137 | } 138 | 139 | void setupOTA() { 140 | ArduinoOTA.onStart([]() { Serial.println("Start"); }); 141 | ArduinoOTA.onEnd([]() { Serial.println("\nEnd"); }); 142 | ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { 143 | Serial.printf("Progress: %u%%\r", (progress / (total / 100))); 144 | }); 145 | ArduinoOTA.onError([](ota_error_t error) { 146 | Serial.printf("Error[%u]: ", error); 147 | if (error == OTA_AUTH_ERROR) { 148 | Serial.println("Auth Failed"); 149 | } else if (error == OTA_BEGIN_ERROR) { 150 | Serial.println("Begin Failed"); 151 | } else if (error == OTA_CONNECT_ERROR) { 152 | Serial.println("Connect Failed"); 153 | } else if (error == OTA_RECEIVE_ERROR) { 154 | Serial.println("Receive Failed"); 155 | } else if (error == OTA_END_ERROR) { 156 | Serial.println("End Failed"); 157 | } 158 | }); 159 | 160 | // This needs a regular string. 161 | ArduinoOTA.setHostname(String(WiFi.getHostname()).c_str()); 162 | 163 | // This could also be a setting 164 | ArduinoOTA.setPassword(identifier); 165 | ArduinoOTA.begin(); 166 | } 167 | 168 | void loop() 169 | { 170 | if (WiFi.isConnected()) 171 | { 172 | if (!set_save_credentials) 173 | { 174 | // Connected to WiFi, but credentials for save/reset etc. were not set 175 | webServer.set_credentials("admin", identifier); 176 | set_save_credentials = true; 177 | } 178 | } 179 | else if (WiFi.softAPgetStationNum() != 0) 180 | { 181 | if (set_save_credentials) 182 | { 183 | // In Soft AP mode with at least one client connected, and a password was set. 184 | webServer.set_credentials(String(), String()); 185 | } 186 | } 187 | 188 | ArduinoOTA.handle(); 189 | webServer.loop(); 190 | 191 | for (auto &device : devices) 192 | { 193 | if (device->is_enabled()) 194 | { 195 | device->loop(); 196 | } 197 | } 198 | 199 | if (factory_reset_next_loop && millis() - restart_reset_when > restart_reset_delay) 200 | { 201 | // Clear file system. 202 | LittleFS.format(); 203 | // Erase configuration 204 | ESP.eraseConfig(); 205 | // Reset (not reboot, that may save current state) 206 | ESP.reset(); 207 | } 208 | 209 | if (restart_next_loop && millis() - restart_reset_when > restart_reset_delay) 210 | { 211 | ESP.restart(); 212 | } 213 | } 214 | 215 | static void on_factory_reset(::grmcdorman::WebSettings &) 216 | { 217 | factory_reset_next_loop = true; 218 | restart_reset_when = millis(); 219 | } 220 | 221 | static void on_restart(::grmcdorman::WebSettings &) 222 | { 223 | restart_next_loop =-true; 224 | restart_reset_when = millis(); 225 | } 226 | 227 | static void on_save(::grmcdorman::WebSettings &) 228 | { 229 | config.save(devices); 230 | } 231 | --------------------------------------------------------------------------------