├── .github └── workflows │ └── update-issue-comment.yml ├── .gitignore ├── LICENSE ├── README.md ├── esp8266_iot_button.ino ├── esp8266_iot_button_actions.ino └── esp8266_iot_button_thing.ino /.github/workflows/update-issue-comment.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow that is manually triggered 2 | 3 | name: IoT Button Press Workflow 4 | 5 | # Controls when the action will run. Workflow runs when manually triggered using the UI 6 | # or API. 7 | on: 8 | workflow_dispatch: 9 | # Inputs the workflow accepts. 10 | inputs: 11 | button_name: 12 | # Friendly description to be shown in the UI instead of 'name' 13 | description: 'Button Name' 14 | # Default value if no value is explicitly provided 15 | default: 'Sparkfun Thing' 16 | # Input has to be provided for the workflow to run 17 | required: true 18 | 19 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 20 | jobs: 21 | createorupdatecomment: 22 | # The type of runner that the job will run on 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Get Time 26 | id: time 27 | uses: nanzm/get-time-action@v1.1 28 | with: 29 | timeZone: 8 30 | format: 'YYYY-MM-DD-HH-mm-ss' 31 | - name: Usage 32 | env: 33 | IME: "${{ steps.time.outputs.time }}" 34 | run: | 35 | echo $TIME 36 | - name: Find Comment 37 | uses: peter-evans/find-comment@v1 38 | id: fc 39 | with: 40 | issue-number: 76 41 | comment-author: 'github-actions[bot]' 42 | body-includes: The ${{ github.event.inputs.button_name }} button was pressed! 43 | 44 | - name: Create comment 45 | if: ${{ steps.fc.outputs.comment-id == 0 }} 46 | uses: peter-evans/create-or-update-comment@v1 47 | with: 48 | issue-number: 76 49 | body: | 50 | The ${{ github.event.inputs.button_name }} button was pressed! 51 | - **Yay!** :sparkles: 52 | - Pressed at: ${{ steps.time.outputs.time }} 53 | reaction-type: "rocket" 54 | 55 | - name: Update comment 56 | if: ${{ steps.fc.outputs.comment-id != 0 }} 57 | uses: peter-evans/create-or-update-comment@v1 58 | with: 59 | comment-id: ${{ steps.fc.outputs.comment-id }} 60 | body: | 61 | **Edit:** The ${{ github.event.inputs.button_name }} button was pressed again at ${{ steps.time.outputs.time }}! 62 | reaction-type: "rocket" 63 | 64 | # This workflow contains a single job called "greet" 65 | greet: 66 | # The type of runner that the job will run on 67 | runs-on: ubuntu-latest 68 | 69 | # Steps represent a sequence of tasks that will be executed as part of the job 70 | steps: 71 | - name: View context attributes 72 | uses: actions/github-script@v3 73 | with: 74 | script: console.log(context) 75 | 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Lua sources 2 | luac.out 3 | 4 | # luarocks build files 5 | *.src.rock 6 | *.zip 7 | *.tar.gz 8 | 9 | # Object files 10 | *.o 11 | *.os 12 | *.ko 13 | *.obj 14 | *.elf 15 | 16 | # Precompiled Headers 17 | *.gch 18 | *.pch 19 | 20 | # Libraries 21 | *.lib 22 | *.a 23 | *.la 24 | *.lo 25 | *.def 26 | *.exp 27 | 28 | # Shared objects (inc. Windows DLLs) 29 | *.dll 30 | *.so 31 | *.so.* 32 | *.dylib 33 | 34 | # Executables 35 | *.exe 36 | *.out 37 | *.app 38 | *.i*86 39 | *.x86_64 40 | *.hex 41 | 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Garth Vander Houwen 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # esp8266 IoT Button using Github Actions 2 | 3 | ![Enclosure Assembly](http://garthvh.com/assets/img/esp8266/sparkfun_thing.jpg "Thing Enclosure") 4 | [Customized Project Enclosure - Thingiverse](http://www.thingiverse.com/thing:981124) 5 | 6 | Tested with Adafruit Huzzah, Sparkfun Thing and WEMOS D1 Mini 7 | 8 | ## Software 9 | 10 | An IoT Button using an ESP8266 WiFI microcontroller, a push button and github actions. POSTS to a [GitHub Actions Workflow dispatch event]( https://docs.github.com/en/rest/reference/actions#create-a-workflow-dispatch-event) endpoint which updates a comment on an issue specific to each button. 11 | 12 | I originally built this project using the IFTTT maker channel sending text messages, then switched to updating a github issue once text messages were no longer free. Now that IFTTT itself is no longer free I wrote a third sketch using GitHub Actions. This makes for a simpler demo anyways as the only service involved is GitHub and everything is stored in code. You can see the ReadMe with instructions from the original IFTTT project [here](https://github.com/garthvh/esp8266button/wiki/Original-IFTTT-Readme). 13 | 14 | The github actions workflow YML file update-issue-comment.yml is also in this repository and you can see the button press comments on the issue i have created for my SparkFun Thing button. 15 | 16 | You will need the following settings in the arduino sketch esp8266_iot_button_actions.ino 17 | 18 | // WiFi and GitHub Variables 19 | const char* ssid = "YOUR_SSID"; // SSID For the WiFi network you want to connect to 20 | const char* password = "YOUR_PSK"; // PSK For the WiFi network you want to connect to 21 | const char* github_user = "YOUR_GITHUB_USERNAME"; // GitHub User for running action 22 | const char* github_repo = "YOUR_GITHUB_REPO"; // GitHub Repo where action lives 23 | const char* github_token = "YOUR_GITHUB_TOKEN"; // GitHub Authorization Token 24 | const char* github_workflow_id = "YOUR_WORKFLOW_YML_FILE"; // Workflow YML file name 25 | const char* host = "api.github.com"; // Server from which data is to be fetched 26 | const int httpsPort = 443; // Default port for HTTPS 27 | 28 | 29 | You will also need to copy the YML from the action in this repository and create a new issue where the comments from the button will go. The issue number in the YML file should be updated to the number of the newly created issue. 30 | 31 | ## Hardware 32 | 33 | I soldered male pins on my Huzzah, and added female headers to my Sparkfun Thing. The thing did not come with any headers and male headers were included with the Huzzah. 34 | 35 | I was able to program both with my [FTDI Friend](https://www.adafruit.com/product/284), you will need to cut the default RTS jumper on the back of the FTDI Friend (used by the Huzzah) and connect the DTR jumper to program the thing. Once cut it has been pretty easy to switch back and forth by soldering the jumpers as needed. 36 | 37 | ![Enclosure Assembly](http://garthvh.com/assets/img/esp8266/button_assembly.jpg "Huzzah Assembly") 38 | 39 | The built in battery and charging circuit on the ESP8266 Thing really makes it easy to work with. By cutting the DTR trace on the bottom of the board and installing pins for a jumper I am able to program the thing with the jumper installed, and debug over serial with it removed. 40 | 41 | ![Enclosure Assembly](http://garthvh.com/assets/img/esp8266/sparkfun_thing_circuit.jpg "Thing Assembly") 42 | 43 | ## BOM 44 | 45 | ### Adafruit Huzzah 46 | 47 | * [Adafruit Huzzah](https://www.adafruit.com/products/2471) 48 | * [Arcade Button](https://www.sparkfun.com/products/9339) 49 | * Standard RGB LCD 50 | * 4 M3 20MM Hex Screws 51 | * AAA Battery Pack 52 | * Female Jumper wires 53 | 54 | ### Sparkfun Thing 55 | 56 | * [Sparkfun Thing](https://www.sparkfun.com/products/13231) 57 | * [Panel Mount Push Button](https://www.adafruit.com/products/1504) 58 | * Standard Green LCD 59 | * 4 M3 20MM Hex Screws 60 | * [150 mAh LiPo Battery](https://www.adafruit.com/product/1317) 61 | * Male Jumper wires 62 | 63 | 64 | ## Enclosure 65 | Using this awesome [Parametric and Customizable Project Enclosure](http://www.thingiverse.com/thing:155001) I made customized enclosures that fit the parts I was using for my buttons. 66 | 67 | ![Customized Project Enclosure](http://garthvh.com/assets/img/esp8266/button_enclosure_green.jpg "Customized Project Enclosure") 68 | 69 | [Customized Project Enclosure - Thingiverse](http://www.thingiverse.com/thing:941755) 70 | -------------------------------------------------------------------------------- /esp8266_iot_button.ino: -------------------------------------------------------------------------------- 1 | /*---------------------------------------------------------------------------- 2 | Simple IoT button using the If This Then That(IFTTT) Maker Channel, Sparkfun 3 | ESP8266 Thing or Adafruit Huzzah, a standard Green LED(or onboard green LED) 4 | and a push button. Optional RGB LED for more detailed status indicators 5 | 6 | While attempting to connect to the specified SSID the LCD flashes, 7 | when connected the led remains green, while data is sending the led turns 8 | off, when done it turns back to green. 9 | 10 | If a saved setting is not found in EEPROM for an available WiFi network the 11 | ESP8266 will enter access point mode and allow the user to configure WiFI 12 | ----------------------------------------------------------------------------*/ 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | 21 | ///////////////////////// 22 | // Network Definitions // 23 | ///////////////////////// 24 | const IPAddress AP_IP(192, 168, 1, 1); 25 | const char* AP_SSID = "ESP8266_IOT_BUTTON_SETUP"; 26 | boolean SETUP_MODE; 27 | String SSID_LIST; 28 | DNSServer DNS_SERVER; 29 | ESP8266WebServer WEB_SERVER(80); 30 | 31 | ///////////////////////// 32 | // Device Definitions // 33 | ///////////////////////// 34 | String DEVICE_TITLE = "IFTTT ESP8266 Dash Like Button"; 35 | boolean POWER_SAVE = false; 36 | boolean RGB_LCD = true; 37 | 38 | /////////////////////// 39 | // IFTTT Definitions // 40 | /////////////////////// 41 | const char* IFTTT_URL= "maker.ifttt.com"; 42 | const char* IFTTT_KEY= "YOUR IFTTT_KEY"; 43 | const char* IFTTT_EVENT = "YOUR_IFTTT_EVENT"; 44 | const char* IFTTT_NOTIFICATION_EVENT = "YOUR_IFTTT_NOTIFICATION_EVENT"; 45 | 46 | ///////////////////// 47 | // Pin Definitions // 48 | ///////////////////// 49 | const int LED_GREEN = 5; 50 | // Blue and Red LED Pins if RGB LCD is enabled 51 | const int LED_RED = 0; 52 | const int LED_BLUE = 4; 53 | const int BUTTON_PIN = 2; 54 | 55 | ////////////////////// 56 | // Button Variables // 57 | ////////////////////// 58 | int BUTTON_STATE; 59 | int LAST_BUTTON_STATE = LOW; 60 | long LAST_DEBOUNCE_TIME = 0; 61 | long DEBOUNCE_DELAY = 50; 62 | int BUTTON_COUNTER = 0; 63 | 64 | void setup() { 65 | 66 | initHardware(); 67 | // Try and restore saved settings 68 | if (loadSavedConfig()) { 69 | if (checkWiFiConnection()) { 70 | SETUP_MODE = false; 71 | startWebServer(); 72 | // Turn the status led Green when the WiFi has been connected 73 | digitalWrite(LED_GREEN, HIGH); 74 | return; 75 | } 76 | } 77 | SETUP_MODE = true; 78 | setupMode(); 79 | } 80 | 81 | void loop() { 82 | 83 | // Handle WiFi Setup and Webserver for reset page 84 | if (SETUP_MODE) { 85 | DNS_SERVER.processNextRequest(); 86 | } 87 | WEB_SERVER.handleClient(); 88 | 89 | // Wait for button Presses 90 | boolean pressed = debounce(); 91 | if (pressed == true) { 92 | BUTTON_COUNTER++; 93 | Serial.print("Trigger" + String(IFTTT_EVENT) + " Event Pressed "); 94 | Serial.print(BUTTON_COUNTER); 95 | Serial.println(" times"); 96 | if(BUTTON_COUNTER > 1) 97 | { 98 | // Turn off the Green LED while transmitting. 99 | digitalWrite(LED_GREEN, LOW); 100 | if(RGB_LCD == true){ 101 | digitalWrite(LED_BLUE, HIGH); 102 | } 103 | triggerButtonEvent(IFTTT_EVENT); 104 | // After a successful send turn the light back to green 105 | if(RGB_LCD == true){ 106 | digitalWrite(LED_BLUE, LOW); 107 | } 108 | digitalWrite(LED_GREEN, HIGH); 109 | } 110 | } 111 | } 112 | 113 | void initHardware() 114 | { 115 | // Serial and EEPROM 116 | Serial.begin(115200); 117 | EEPROM.begin(512); 118 | delay(10); 119 | // LEDS 120 | pinMode(LED_GREEN, OUTPUT); 121 | digitalWrite(LED_GREEN, LOW); 122 | if(RGB_LCD == true){ 123 | pinMode(LED_RED, OUTPUT); 124 | digitalWrite(LED_RED, LOW); 125 | pinMode(LED_BLUE, OUTPUT); 126 | digitalWrite(LED_BLUE, LOW); 127 | } 128 | // Button 129 | pinMode(BUTTON_PIN, INPUT); 130 | 131 | } 132 | 133 | ////////////////////// 134 | // Button Functions // 135 | ////////////////////// 136 | void triggerButtonEvent(String eventName) 137 | { 138 | // Define the WiFi Client 139 | WiFiClient client; 140 | // Set the http Port 141 | const int httpPort = 80; 142 | 143 | // Make sure we can connect 144 | if (!client.connect(IFTTT_URL, httpPort)) { 145 | return; 146 | } 147 | 148 | // We now create a URI for the request 149 | String url = "/trigger/" + String(eventName) + "/with/key/" + String(IFTTT_KEY); 150 | 151 | // Set some values for the JSON data depending on which event has been triggered 152 | IPAddress ip = WiFi.localIP(); 153 | String ipStr = String(ip[0]) + '.' + String(ip[1]) + '.' + String(ip[2]) + '.' + String(ip[3]); 154 | String value_1 = ""; 155 | String value_2 = ""; 156 | String value_3 = ""; 157 | if(eventName == IFTTT_EVENT){ 158 | value_1 = String(BUTTON_COUNTER -1); 159 | value_2 = ipStr; 160 | value_3 = WiFi.gatewayIP().toString(); 161 | } 162 | else if(eventName == IFTTT_NOTIFICATION_EVENT){ 163 | value_1 = ipStr; 164 | value_2 = WiFi.SSID(); 165 | } 166 | 167 | 168 | // Build JSON data string 169 | String data = ""; 170 | data = data + "\n" + "{\"value1\":\""+ value_1 +"\",\"value2\":\""+ value_2 +"\",\"value3\":\""+ value_3 + "\"}"; 171 | 172 | // Post the button press to IFTTT 173 | if (client.connect(IFTTT_URL, httpPort)) { 174 | 175 | // Sent HTTP POST Request with JSON data 176 | client.println("POST "+ url +" HTTP/1.1"); 177 | Serial.println("POST "+ url +" HTTP/1.1"); 178 | client.println("Host: "+ String(IFTTT_URL)); 179 | Serial.println("Host: "+ String(IFTTT_URL)); 180 | client.println("User-Agent: Arduino/1.0"); 181 | Serial.println("User-Agent: Arduino/1.0"); 182 | client.print("Accept: *"); 183 | Serial.print("Accept: *"); 184 | client.print("/"); 185 | Serial.print("/"); 186 | client.println("*"); 187 | Serial.println("*"); 188 | client.print("Content-Length: "); 189 | Serial.print("Content-Length: "); 190 | client.println(data.length()); 191 | Serial.println(data.length()); 192 | client.println("Content-Type: application/json"); 193 | Serial.println("Content-Type: application/json"); 194 | client.println("Connection: close"); 195 | Serial.println("Connection: close"); 196 | client.println(); 197 | Serial.println(); 198 | client.println(data); 199 | Serial.println(data); 200 | } 201 | } 202 | 203 | // Debounce Button Presses 204 | boolean debounce() { 205 | boolean retVal = false; 206 | int reading = digitalRead(BUTTON_PIN); 207 | if (reading != LAST_BUTTON_STATE) { 208 | LAST_DEBOUNCE_TIME = millis(); 209 | } 210 | if ((millis() - LAST_DEBOUNCE_TIME) > DEBOUNCE_DELAY) { 211 | if (reading != BUTTON_STATE) { 212 | BUTTON_STATE = reading; 213 | if (BUTTON_STATE == HIGH) { 214 | retVal = true; 215 | } 216 | } 217 | } 218 | LAST_BUTTON_STATE = reading; 219 | return retVal; 220 | } 221 | 222 | ///////////////////////////// 223 | // AP Setup Mode Functions // 224 | ///////////////////////////// 225 | 226 | // Load Saved Configuration from EEPROM 227 | boolean loadSavedConfig() { 228 | Serial.println("Reading Saved Config...."); 229 | String ssid = ""; 230 | String password = ""; 231 | if (EEPROM.read(0) != 0) { 232 | for (int i = 0; i < 32; ++i) { 233 | ssid += char(EEPROM.read(i)); 234 | } 235 | Serial.print("SSID: "); 236 | Serial.println(ssid); 237 | for (int i = 32; i < 96; ++i) { 238 | password += char(EEPROM.read(i)); 239 | } 240 | Serial.print("Password: "); 241 | Serial.println(password); 242 | WiFi.begin(ssid.c_str(), password.c_str()); 243 | return true; 244 | } 245 | else { 246 | Serial.println("Saved Configuration not found."); 247 | return false; 248 | } 249 | } 250 | 251 | // Boolean function to check for a WiFi Connection 252 | boolean checkWiFiConnection() { 253 | int count = 0; 254 | Serial.print("Waiting to connect to the specified WiFi network"); 255 | while ( count < 30 ) { 256 | if (WiFi.status() == WL_CONNECTED) { 257 | Serial.println(); 258 | Serial.println("Connected!"); 259 | return (true); 260 | } 261 | delay(500); 262 | Serial.print("."); 263 | count++; 264 | } 265 | Serial.println("Timed out."); 266 | return false; 267 | } 268 | 269 | // Start the web server and build out pages 270 | void startWebServer() { 271 | if (SETUP_MODE) { 272 | Serial.print("Starting Web Server at IP address: "); 273 | Serial.println(WiFi.softAPIP()); 274 | // Settings Page 275 | WEB_SERVER.on("/settings", []() { 276 | String s = "

Wi-Fi Settings

Please select the SSID of the network you wish to connect to and then enter the password and submit.

"; 277 | s += "


Password:

"; 280 | WEB_SERVER.send(200, "text/html", makePage("Wi-Fi Settings", s)); 281 | }); 282 | // setap Form Post 283 | WEB_SERVER.on("/setap", []() { 284 | for (int i = 0; i < 96; ++i) { 285 | EEPROM.write(i, 0); 286 | } 287 | String ssid = urlDecode(WEB_SERVER.arg("ssid")); 288 | Serial.print("SSID: "); 289 | Serial.println(ssid); 290 | String pass = urlDecode(WEB_SERVER.arg("pass")); 291 | Serial.print("Password: "); 292 | Serial.println(pass); 293 | Serial.println("Writing SSID to EEPROM..."); 294 | for (int i = 0; i < ssid.length(); ++i) { 295 | EEPROM.write(i, ssid[i]); 296 | } 297 | Serial.println("Writing Password to EEPROM..."); 298 | for (int i = 0; i < pass.length(); ++i) { 299 | EEPROM.write(32 + i, pass[i]); 300 | } 301 | EEPROM.commit(); 302 | Serial.println("Write EEPROM done!"); 303 | String s = "

WiFi Setup complete.

The button will be connected automatically to \""; 304 | s += ssid; 305 | s += "\" after the restart."; 306 | WEB_SERVER.send(200, "text/html", makePage("Wi-Fi Settings", s)); 307 | ESP.restart(); 308 | }); 309 | // Show the configuration page if no path is specified 310 | WEB_SERVER.onNotFound([]() { 311 | String s = "

WiFi Configuration Mode

Wi-Fi Settings

"; 312 | WEB_SERVER.send(200, "text/html", makePage("Access Point mode", s)); 313 | }); 314 | } 315 | else { 316 | Serial.print("Starting Web Server at "); 317 | Serial.println(WiFi.localIP()); 318 | WEB_SERVER.on("/", []() { 319 | IPAddress ip = WiFi.localIP(); 320 | String ipStr = String(ip[0]) + '.' + String(ip[1]) + '.' + String(ip[2]) + '.' + String(ip[3]); 321 | String s = "

IoT Button Status

"; 322 | s += "

Network Details

"; 323 | s += "

Connected to: " + String(WiFi.SSID()) + "

"; 324 | s += "

IP Address: " + ipStr + "

"; 325 | s += "

Button Details

"; 326 | s += "

Event Name: " + String(IFTTT_EVENT) + "

"; 327 | s += "

Button Presses: " + String(BUTTON_COUNTER - 1) + "

"; 328 | s += "

Options

"; 329 | s += "

Clear Saved Wi-Fi Settings

"; 330 | WEB_SERVER.send(200, "text/html", makePage("Station mode", s)); 331 | }); 332 | WEB_SERVER.on("/reset", []() { 333 | for (int i = 0; i < 96; ++i) { 334 | EEPROM.write(i, 0); 335 | } 336 | EEPROM.commit(); 337 | String s = "

Wi-Fi settings was reset.

Please reset device.

"; 338 | WEB_SERVER.send(200, "text/html", makePage("Reset Wi-Fi Settings", s)); 339 | }); 340 | } 341 | WEB_SERVER.begin(); 342 | triggerButtonEvent(IFTTT_NOTIFICATION_EVENT); 343 | } 344 | 345 | // Build the SSID list and setup a software access point for setup mode 346 | void setupMode() { 347 | WiFi.mode(WIFI_STA); 348 | WiFi.disconnect(); 349 | delay(100); 350 | int n = WiFi.scanNetworks(); 351 | delay(100); 352 | Serial.println(""); 353 | for (int i = 0; i < n; ++i) { 354 | SSID_LIST += ""; 359 | } 360 | delay(100); 361 | WiFi.mode(WIFI_AP); 362 | WiFi.softAPConfig(AP_IP, AP_IP, IPAddress(255, 255, 255, 0)); 363 | WiFi.softAP(AP_SSID); 364 | DNS_SERVER.start(53, "*", AP_IP); 365 | startWebServer(); 366 | Serial.print("Starting Access Point at \""); 367 | Serial.print(AP_SSID); 368 | Serial.println("\""); 369 | } 370 | 371 | String makePage(String title, String contents) { 372 | String s = ""; 373 | s += ""; 374 | s += ""; 382 | s += ""; 383 | s += title; 384 | s += ""; 385 | s += "

" + DEVICE_TITLE + "

"; 386 | s += "
"; 387 | s += contents; 388 | s += "
"; 389 | s += ""; 390 | return s; 391 | } 392 | 393 | String urlDecode(String input) { 394 | String s = input; 395 | s.replace("%20", " "); 396 | s.replace("+", " "); 397 | s.replace("%21", "!"); 398 | s.replace("%22", "\""); 399 | s.replace("%23", "#"); 400 | s.replace("%24", "$"); 401 | s.replace("%25", "%"); 402 | s.replace("%26", "&"); 403 | s.replace("%27", "\'"); 404 | s.replace("%28", "("); 405 | s.replace("%29", ")"); 406 | s.replace("%30", "*"); 407 | s.replace("%31", "+"); 408 | s.replace("%2C", ","); 409 | s.replace("%2E", "."); 410 | s.replace("%2F", "/"); 411 | s.replace("%2C", ","); 412 | s.replace("%3A", ":"); 413 | s.replace("%3A", ";"); 414 | s.replace("%3C", "<"); 415 | s.replace("%3D", "="); 416 | s.replace("%3E", ">"); 417 | s.replace("%3F", "?"); 418 | s.replace("%40", "@"); 419 | s.replace("%5B", "["); 420 | s.replace("%5C", "\\"); 421 | s.replace("%5D", "]"); 422 | s.replace("%5E", "^"); 423 | s.replace("%5F", "-"); 424 | s.replace("%60", "`"); 425 | return s; 426 | } 427 | 428 | ///////////////////////// 429 | // Debugging Functions // 430 | ///////////////////////// 431 | 432 | void wipeEEPROM() 433 | { 434 | EEPROM.begin(512); 435 | // write a 0 to all 512 bytes of the EEPROM 436 | for (int i = 0; i < 512; i++) 437 | EEPROM.write(i, 0); 438 | 439 | EEPROM.end(); 440 | } 441 | -------------------------------------------------------------------------------- /esp8266_iot_button_actions.ino: -------------------------------------------------------------------------------- 1 | /* 2 | * Use HTTPS to trigger a GitHub Action 3 | * 4 | * Created by Garth Vander Houwen, 2021. 5 | * 6 | */ 7 | // Libraries 8 | #include 9 | #include 10 | #include 11 | 12 | // WiFi and GitHub Variables 13 | const char* ssid = "YOUR_SSID"; // SSID For the WiFi network you want to connect to 14 | const char* password = "YOUR_PSK"; // PSK For the WiFi network you want to connect to 15 | const char* github_user = "YOUR_GITHUB_USERNAME"; // GitHub User for running action 16 | const char* github_repo = "YOUR_GITHUB_REPO"; // GitHub Repo where action lives 17 | const char* github_token = "YOUR_GITHUB_TOKEN"; // GitHub Authorization Token 18 | const char* github_workflow_id = "YOUR_WORKFLOW_YML_FILE"; // Workflow YML file name 19 | const char* host = "api.github.com"; // Server from which data is to be fetched 20 | const int httpsPort = 443; // Default port for HTTPS 21 | 22 | // Use web browser to view and copy 23 | // SHA1 fingerprint of the certificate 24 | const char* fingerprint = "35 85 74 ef 67 35 a7 ce 40 69 50 f3 c0 f6 80 cf 80 3b 2e 19"; // Fingerprint/Thumbprint for website api.github.com 25 | 26 | // Button Variables 27 | const int buttonPin = 0; // pushbutton pin 28 | const int ledPin = 4; // LED pin 29 | 30 | int ledState = LOW; // current state of LED 31 | int buttonState = LOW; // current state of button 32 | int lastButtonState = LOW; // previous state of button 33 | 34 | unsigned long lastDebounceTime = 0; // last toggle 35 | unsigned long debounceDelay = 50; // debounce time 36 | 37 | void setup() { 38 | 39 | // Start Serial 40 | Serial.begin(115200); 41 | Serial.println(); 42 | Serial.println("...."); 43 | Serial.println("...."); 44 | delay (5000); 45 | // Connect WiFi 46 | Serial.print("Connecting to "); 47 | Serial.print(ssid); 48 | WiFi.begin(ssid, password); 49 | while (WiFi.status() != WL_CONNECTED) { 50 | delay(500); 51 | Serial.print("."); 52 | } 53 | Serial.println(""); 54 | Serial.println("WiFi connected"); 55 | Serial.print("IP address: "); 56 | Serial.println(WiFi.localIP()); // Print out the Local IP assigned by the router to ESP8266 57 | } 58 | 59 | void loop() { 60 | int reading = digitalRead(buttonPin); //read 61 | 62 | if (reading != lastButtonState) // If the switch changed 63 | { 64 | lastDebounceTime = millis(); // reset the debouncing 65 | } 66 | //check if debounce time > 50ms 67 | if ((millis() - lastDebounceTime) > debounceDelay) 68 | { if (reading != buttonState) 69 | { buttonState = reading; 70 | if (buttonState == LOW) 71 | { 72 | Serial.println("button pressed"); 73 | SendDispatch(); 74 | } 75 | } 76 | } 77 | lastButtonState = reading; // save the reading 78 | } 79 | 80 | void SendDispatch(){ 81 | 82 | // Setup HTTPS Client 83 | WiFiClientSecure client; // Use WiFiClientSecure class to create client instance 84 | Serial.print("connecting to "); 85 | Serial.println(host); 86 | client.setInsecure(); 87 | 88 | // Connect with the server api.github.com at port 443 89 | if (!client.connect(host, httpsPort)) { 90 | Serial.println("connection failed"); 91 | return; 92 | } 93 | 94 | // Verify fingerprint 95 | if (client.verify(fingerprint, host)) { 96 | Serial.println("certificate matches"); 97 | } 98 | else { 99 | Serial.println("certificate doesn't match"); 100 | } 101 | 102 | // Execute a POST request to create a workflow dispatch event 103 | // https://docs.github.com/en/rest/reference/actions#create-a-workflow-dispatch-event 104 | 105 | // Parameters 106 | // Name Type In Description 107 | // ------------|-------|-------|---------- 108 | // accept string header Setting to application/vnd.github.v3+json is recommended. 109 | // owner string path 110 | // repo string path 111 | // workflow_id string path The ID of the workflow. You can also pass the workflow file name as a string. 112 | // ref string body Required. The git reference for the workflow. The reference can be a branch or tag name. 113 | // inputs object body Input keys and values configured in the workflow file. The maximum number of properties is 10. Any default properties configured in the workflow file will be used when inputs are omitted. 114 | 115 | // GitHub Actions API URL /repos/{github_user}/{github_repo}/actions/workflows/{github_workflow_id}/dispatches 116 | String url = "https://api.github.com/repos/" + String(github_user) + "/" + String(github_repo) + "/actions/workflows/" + String(github_workflow_id) + "/dispatches"; 117 | 118 | // JSON data with the tag or branch you want the action to run against pass up to 10 custom inputs 119 | String data = "{\"ref\":\"main\",\"inputs\":{\"button_name\":\"SparkFun Thing #76\"}}"; 120 | 121 | HTTPClient https; 122 | Serial.println("Starting https client"); 123 | if (https.begin(client, url)) { 124 | https.addHeader("Content-Type", "application/json"); 125 | https.addHeader("Authorization", "token " + String(github_token)); 126 | Serial.println("posting to the github api"); 127 | int httpCode = https.POST(data); 128 | // httpCode will be negative on error 129 | if (httpCode > 0) { 130 | // HTTP header has been send and Server response header has been handled 131 | Serial.printf("POST http status code: %d\n", httpCode); 132 | // handle response 133 | if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_NO_CONTENT || HTTP_CODE_MOVED_PERMANENTLY) { 134 | String payload = https.getString(); 135 | Serial.println("successfully posted the following JSON data"); 136 | Serial.println(data); 137 | } 138 | } 139 | else { 140 | Serial.printf("POST failed, error: %s\n", https.errorToString(httpCode).c_str()); 141 | String payload = https.getString(); 142 | Serial.println("failed to post the following JSON data"); 143 | Serial.println(payload); 144 | } 145 | https.end(); 146 | } 147 | else { 148 | Serial.printf("unable to connect\n"); 149 | } 150 | Serial.println("closing connection"); 151 | 152 | } 153 | -------------------------------------------------------------------------------- /esp8266_iot_button_thing.ino: -------------------------------------------------------------------------------- 1 | /*---------------------------------------------------------------------------- 2 | Simple IoT button using the If This Then That(IFTTT) Maker Channel, Sparkfun 3 | ESP8266 Thing or Adafruit Huzzah, a standard Green LED(or onboard green LED) 4 | and a push button. Optional RGB LED for more detailed status indicators 5 | 6 | While attempting to connect to the specified SSID the LCD flashes, 7 | when connected the led remains green, while data is sending the led turns 8 | off, when done it turns back to green. 9 | 10 | If a saved setting is not found in EEPROM for an available WiFi network the 11 | ESP8266 will enter access point mode and allow the user to configure WiFI 12 | ----------------------------------------------------------------------------*/ 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | 21 | ///////////////////////// 22 | // Network Definitions // 23 | ///////////////////////// 24 | const IPAddress AP_IP(192, 168, 1, 1); 25 | const char* AP_SSID = "ESP8266_IOT_BUTTON_SETUP"; 26 | boolean SETUP_MODE; 27 | String SSID_LIST; 28 | DNSServer DNS_SERVER; 29 | ESP8266WebServer WEB_SERVER(80); 30 | 31 | ///////////////////////// 32 | // Device Definitions // 33 | ///////////////////////// 34 | String DEVICE_TITLE = "IFTTT ESP8266 Dash Like Button"; 35 | boolean POWER_SAVE = false; 36 | boolean RGB_LCD = false; 37 | 38 | /////////////////////// 39 | // IFTTT Definitions // 40 | /////////////////////// 41 | const char* IFTTT_URL= "maker.ifttt.com"; 42 | const char* IFTTT_KEY= "YOUR IFTTT_KEY"; 43 | const char* IFTTT_EVENT = "YOUR_IFTTT_EVENT"; 44 | const char* IFTTT_NOTIFICATION_EVENT = "YOUR_IFTTT_NOTIFICATION_EVENT"; 45 | 46 | ///////////////////// 47 | // Pin Definitions // 48 | ///////////////////// 49 | const int LED_GREEN = 5; 50 | // Blue and Red LED Pins if RGB LCD is enabled 51 | const int LED_RED = 2; 52 | const int LED_BLUE = 4; 53 | const int BUTTON_PIN = 0; 54 | 55 | ////////////////////// 56 | // Button Variables // 57 | ////////////////////// 58 | int BUTTON_STATE; 59 | int LAST_BUTTON_STATE = LOW; 60 | long LAST_DEBOUNCE_TIME = 0; 61 | long DEBOUNCE_DELAY = 50; 62 | int BUTTON_COUNTER = 0; 63 | 64 | void setup() { 65 | 66 | initHardware(); 67 | // Try and restore saved settings 68 | if (loadSavedConfig()) { 69 | if (checkWiFiConnection()) { 70 | SETUP_MODE = false; 71 | startWebServer(); 72 | // Turn the status led Green when the WiFi has been connected 73 | digitalWrite(LED_GREEN, HIGH); 74 | return; 75 | } 76 | } 77 | SETUP_MODE = true; 78 | setupMode(); 79 | } 80 | 81 | void loop() { 82 | 83 | // Handle WiFi Setup and Webserver for reset page 84 | if (SETUP_MODE) { 85 | DNS_SERVER.processNextRequest(); 86 | } 87 | WEB_SERVER.handleClient(); 88 | 89 | // Wait for button Presses 90 | boolean pressed = debounce(); 91 | if (pressed == true) { 92 | BUTTON_COUNTER++; 93 | Serial.print("Trigger" + String(IFTTT_EVENT) + " Event Pressed "); 94 | Serial.print(BUTTON_COUNTER); 95 | Serial.println(" times"); 96 | if(BUTTON_COUNTER > 1) 97 | { 98 | // Turn off the Green LED while transmitting. 99 | digitalWrite(LED_GREEN, LOW); 100 | if(RGB_LCD == true){ 101 | digitalWrite(LED_BLUE, HIGH); 102 | } 103 | triggerButtonEvent(IFTTT_EVENT); 104 | // After a successful send turn the light back to green 105 | if(RGB_LCD == true){ 106 | digitalWrite(LED_BLUE, LOW); 107 | } 108 | digitalWrite(LED_GREEN, HIGH); 109 | } 110 | } 111 | } 112 | 113 | void initHardware() 114 | { 115 | // Serial and EEPROM 116 | Serial.begin(115200); 117 | EEPROM.begin(512); 118 | delay(10); 119 | // LEDS 120 | pinMode(LED_GREEN, OUTPUT); 121 | digitalWrite(LED_GREEN, LOW); 122 | if(RGB_LCD == true){ 123 | pinMode(LED_RED, OUTPUT); 124 | digitalWrite(LED_RED, LOW); 125 | pinMode(LED_BLUE, OUTPUT); 126 | digitalWrite(LED_BLUE, LOW); 127 | } 128 | // Button 129 | pinMode(BUTTON_PIN, INPUT); 130 | 131 | } 132 | 133 | ////////////////////// 134 | // Button Functions // 135 | ////////////////////// 136 | void triggerButtonEvent(String eventName) 137 | { 138 | // Define the WiFi Client 139 | WiFiClient client; 140 | // Set the http Port 141 | const int httpPort = 80; 142 | 143 | // Make sure we can connect 144 | if (!client.connect(IFTTT_URL, httpPort)) { 145 | return; 146 | } 147 | 148 | // We now create a URI for the request 149 | String url = "/trigger/" + String(eventName) + "/with/key/" + String(IFTTT_KEY); 150 | 151 | // Set some values for the JSON data depending on which event has been triggered 152 | IPAddress ip = WiFi.localIP(); 153 | String ipStr = String(ip[0]) + '.' + String(ip[1]) + '.' + String(ip[2]) + '.' + String(ip[3]); 154 | String value_1 = ""; 155 | String value_2 = ""; 156 | String value_3 = ""; 157 | if(eventName == IFTTT_EVENT){ 158 | value_1 = String(BUTTON_COUNTER -1); 159 | value_2 = ipStr; 160 | value_3 = WiFi.gatewayIP().toString(); 161 | } 162 | else if(eventName == IFTTT_NOTIFICATION_EVENT){ 163 | value_1 = ipStr; 164 | value_2 = WiFi.SSID(); 165 | } 166 | 167 | 168 | // Build JSON data string 169 | String data = ""; 170 | data = data + "\n" + "{\"value1\":\""+ value_1 +"\",\"value2\":\""+ value_2 +"\",\"value3\":\""+ value_3 + "\"}"; 171 | 172 | // Post the button press to IFTTT 173 | if (client.connect(IFTTT_URL, httpPort)) { 174 | 175 | // Sent HTTP POST Request with JSON data 176 | client.println("POST "+ url +" HTTP/1.1"); 177 | Serial.println("POST "+ url +" HTTP/1.1"); 178 | client.println("Host: "+ String(IFTTT_URL)); 179 | Serial.println("Host: "+ String(IFTTT_URL)); 180 | client.println("User-Agent: Arduino/1.0"); 181 | Serial.println("User-Agent: Arduino/1.0"); 182 | client.print("Accept: *"); 183 | Serial.print("Accept: *"); 184 | client.print("/"); 185 | Serial.print("/"); 186 | client.println("*"); 187 | Serial.println("*"); 188 | client.print("Content-Length: "); 189 | Serial.print("Content-Length: "); 190 | client.println(data.length()); 191 | Serial.println(data.length()); 192 | client.println("Content-Type: application/json"); 193 | Serial.println("Content-Type: application/json"); 194 | client.println("Connection: close"); 195 | Serial.println("Connection: close"); 196 | client.println(); 197 | Serial.println(); 198 | client.println(data); 199 | Serial.println(data); 200 | } 201 | } 202 | 203 | // Debounce Button Presses 204 | boolean debounce() { 205 | boolean retVal = false; 206 | int reading = digitalRead(BUTTON_PIN); 207 | if (reading != LAST_BUTTON_STATE) { 208 | LAST_DEBOUNCE_TIME = millis(); 209 | } 210 | if ((millis() - LAST_DEBOUNCE_TIME) > DEBOUNCE_DELAY) { 211 | if (reading != BUTTON_STATE) { 212 | BUTTON_STATE = reading; 213 | if (BUTTON_STATE == HIGH) { 214 | retVal = true; 215 | } 216 | } 217 | } 218 | LAST_BUTTON_STATE = reading; 219 | return retVal; 220 | } 221 | 222 | ///////////////////////////// 223 | // AP Setup Mode Functions // 224 | ///////////////////////////// 225 | 226 | // Load Saved Configuration from EEPROM 227 | boolean loadSavedConfig() { 228 | Serial.println("Reading Saved Config...."); 229 | String ssid = ""; 230 | String password = ""; 231 | if (EEPROM.read(0) != 0) { 232 | for (int i = 0; i < 32; ++i) { 233 | ssid += char(EEPROM.read(i)); 234 | } 235 | Serial.print("SSID: "); 236 | Serial.println(ssid); 237 | for (int i = 32; i < 96; ++i) { 238 | password += char(EEPROM.read(i)); 239 | } 240 | Serial.print("Password: "); 241 | Serial.println(password); 242 | WiFi.begin(ssid.c_str(), password.c_str()); 243 | return true; 244 | } 245 | else { 246 | Serial.println("Saved Configuration not found."); 247 | return false; 248 | } 249 | } 250 | 251 | // Boolean function to check for a WiFi Connection 252 | boolean checkWiFiConnection() { 253 | int count = 0; 254 | Serial.print("Waiting to connect to the specified WiFi network"); 255 | while ( count < 30 ) { 256 | if (WiFi.status() == WL_CONNECTED) { 257 | Serial.println(); 258 | Serial.println("Connected!"); 259 | return (true); 260 | } 261 | delay(500); 262 | Serial.print("."); 263 | count++; 264 | } 265 | Serial.println("Timed out."); 266 | return false; 267 | } 268 | 269 | // Start the web server and build out pages 270 | void startWebServer() { 271 | if (SETUP_MODE) { 272 | Serial.print("Starting Web Server at IP address: "); 273 | Serial.println(WiFi.softAPIP()); 274 | // Settings Page 275 | WEB_SERVER.on("/settings", []() { 276 | String s = "

Wi-Fi Settings

Please select the SSID of the network you wish to connect to and then enter the password and submit.

"; 277 | s += "


Password:

"; 280 | WEB_SERVER.send(200, "text/html", makePage("Wi-Fi Settings", s)); 281 | }); 282 | // setap Form Post 283 | WEB_SERVER.on("/setap", []() { 284 | for (int i = 0; i < 96; ++i) { 285 | EEPROM.write(i, 0); 286 | } 287 | String ssid = urlDecode(WEB_SERVER.arg("ssid")); 288 | Serial.print("SSID: "); 289 | Serial.println(ssid); 290 | String pass = urlDecode(WEB_SERVER.arg("pass")); 291 | Serial.print("Password: "); 292 | Serial.println(pass); 293 | Serial.println("Writing SSID to EEPROM..."); 294 | for (int i = 0; i < ssid.length(); ++i) { 295 | EEPROM.write(i, ssid[i]); 296 | } 297 | Serial.println("Writing Password to EEPROM..."); 298 | for (int i = 0; i < pass.length(); ++i) { 299 | EEPROM.write(32 + i, pass[i]); 300 | } 301 | EEPROM.commit(); 302 | Serial.println("Write EEPROM done!"); 303 | String s = "

WiFi Setup complete.

The button will be connected automatically to \""; 304 | s += ssid; 305 | s += "\" after the restart."; 306 | WEB_SERVER.send(200, "text/html", makePage("Wi-Fi Settings", s)); 307 | ESP.restart(); 308 | }); 309 | // Show the configuration page if no path is specified 310 | WEB_SERVER.onNotFound([]() { 311 | String s = "

WiFi Configuration Mode

Wi-Fi Settings

"; 312 | WEB_SERVER.send(200, "text/html", makePage("Access Point mode", s)); 313 | }); 314 | } 315 | else { 316 | Serial.print("Starting Web Server at "); 317 | Serial.println(WiFi.localIP()); 318 | WEB_SERVER.on("/", []() { 319 | IPAddress ip = WiFi.localIP(); 320 | String ipStr = String(ip[0]) + '.' + String(ip[1]) + '.' + String(ip[2]) + '.' + String(ip[3]); 321 | String s = "

IoT Button Status

"; 322 | s += "

Network Details

"; 323 | s += "

Connected to: " + String(WiFi.SSID()) + "

"; 324 | s += "

IP Address: " + ipStr + "

"; 325 | s += "

Button Details

"; 326 | s += "

Event Name: " + String(IFTTT_EVENT) + "

"; 327 | s += "

Button Presses: " + String(BUTTON_COUNTER - 1) + "

"; 328 | s += "

Options

"; 329 | s += "

Clear Saved Wi-Fi Settings

"; 330 | WEB_SERVER.send(200, "text/html", makePage("Station mode", s)); 331 | }); 332 | WEB_SERVER.on("/reset", []() { 333 | for (int i = 0; i < 96; ++i) { 334 | EEPROM.write(i, 0); 335 | } 336 | EEPROM.commit(); 337 | String s = "

Wi-Fi settings was reset.

Please reset device.

"; 338 | WEB_SERVER.send(200, "text/html", makePage("Reset Wi-Fi Settings", s)); 339 | }); 340 | } 341 | WEB_SERVER.begin(); 342 | triggerButtonEvent(IFTTT_NOTIFICATION_EVENT); 343 | } 344 | 345 | // Build the SSID list and setup a software access point for setup mode 346 | void setupMode() { 347 | WiFi.mode(WIFI_STA); 348 | WiFi.disconnect(); 349 | delay(100); 350 | int n = WiFi.scanNetworks(); 351 | delay(100); 352 | Serial.println(""); 353 | for (int i = 0; i < n; ++i) { 354 | SSID_LIST += ""; 359 | } 360 | delay(100); 361 | WiFi.mode(WIFI_AP); 362 | WiFi.softAPConfig(AP_IP, AP_IP, IPAddress(255, 255, 255, 0)); 363 | WiFi.softAP(AP_SSID); 364 | DNS_SERVER.start(53, "*", AP_IP); 365 | startWebServer(); 366 | Serial.print("Starting Access Point at \""); 367 | Serial.print(AP_SSID); 368 | Serial.println("\""); 369 | } 370 | 371 | String makePage(String title, String contents) { 372 | String s = ""; 373 | s += ""; 374 | s += ""; 382 | s += ""; 383 | s += title; 384 | s += ""; 385 | s += "

" + DEVICE_TITLE + "

"; 386 | s += "
"; 387 | s += contents; 388 | s += "
"; 389 | s += ""; 390 | return s; 391 | } 392 | 393 | String urlDecode(String input) { 394 | String s = input; 395 | s.replace("%20", " "); 396 | s.replace("+", " "); 397 | s.replace("%21", "!"); 398 | s.replace("%22", "\""); 399 | s.replace("%23", "#"); 400 | s.replace("%24", "$"); 401 | s.replace("%25", "%"); 402 | s.replace("%26", "&"); 403 | s.replace("%27", "\'"); 404 | s.replace("%28", "("); 405 | s.replace("%29", ")"); 406 | s.replace("%30", "*"); 407 | s.replace("%31", "+"); 408 | s.replace("%2C", ","); 409 | s.replace("%2E", "."); 410 | s.replace("%2F", "/"); 411 | s.replace("%2C", ","); 412 | s.replace("%3A", ":"); 413 | s.replace("%3A", ";"); 414 | s.replace("%3C", "<"); 415 | s.replace("%3D", "="); 416 | s.replace("%3E", ">"); 417 | s.replace("%3F", "?"); 418 | s.replace("%40", "@"); 419 | s.replace("%5B", "["); 420 | s.replace("%5C", "\\"); 421 | s.replace("%5D", "]"); 422 | s.replace("%5E", "^"); 423 | s.replace("%5F", "-"); 424 | s.replace("%60", "`"); 425 | return s; 426 | } 427 | 428 | ///////////////////////// 429 | // Debugging Functions // 430 | ///////////////////////// 431 | 432 | void wipeEEPROM() 433 | { 434 | EEPROM.begin(512); 435 | // write a 0 to all 512 bytes of the EEPROM 436 | for (int i = 0; i < 512; i++) 437 | EEPROM.write(i, 0); 438 | 439 | EEPROM.end(); 440 | } 441 | --------------------------------------------------------------------------------