Webhook becomes inactive after some time

Hello I have an issue I don't understand.

I made a google sheet project called "Raw"

Then I have 10 devices which each have its own spreadsheet.

Like. Machine_1, Machine_2, etc.

I create a webhook using google script.

I put the webhook link into each of the 10 things I have in arduino iot cloud.

The webhook becomes active and the data starts apearing into the spreadsheets.

After around 2 days the webhook becomes inactive in arduino iot cloud.

Inside arduino iot cloud I click change link, the webhook link is still present and without doing any changes I just press "set webhook" and it works again.

Why does it keep stop working???

Hi, webhook is automatically de-activated if there are more than 20 errors in 10 minutes.
For errors I mean error codes (4xx/5xx) returned by the server to webhook sender.
This could be a possible cause of behavior you are experiencing.
Regards

Thank you!
How can I see the errors so I prevent it?

Okay im getting absolut insane! I cannot figure out how to make this work, it keeps going inactive after some minuts.
I have spend hours looking for a solution but everything I have tried doesn't work, also I cant figure out to get a propper debug result I can rely on because I havn't found any sample code that actually works.

Hello, I have been puzzled by how I can fix these dublicating messages?

Long story short. I have this ESP32 connected to arduino IoT Cloud, from IoT Cloud I send some data through a webhook to a Google Sheet.

I have a bunch of variables but it is only one variable I want to use with google sheet.

The problem is that I can see everytime one of the other variables updates it sends a new message to google sheet, which causes an overflow of errors and makes the webhook link become inactive after few minuts.

In order to make it work I have changed the update interval from every 1 seconds to every 600 seconds for the variables I dont use in google sheet and every 30 seconds for the variable I want to use with google sheet. The webhook no longer becomes inactive but I still recieve dublicated messages.

What can I do to fix this issue?

This is my google script:

// Opret en daglig trigger til at køre funktionen dailyBackupAndClear kl. 06:00
function createDailyTrigger() {
  // Slet eksisterende triggers for at undgå duplikater
  deleteTriggers();

  // Opret en ny trigger, der kører dagligt kl. 06:00
  ScriptApp.newTrigger("dailyBackupAndClear")
    .timeBased()
    .atHour(9) // Kl. 06:00
    .nearMinute(25) // 32 minutter
    .everyDays(1) // Hver dag
    .create();
}

// Slet eksisterende triggers
function deleteTriggers() {
  var triggers = ScriptApp.getProjectTriggers();
  for (var i = 0; i < triggers.length; i++) {
    ScriptApp.deleteTrigger(triggers[i]);
  }
}

// Daglig funktion til at kopiere hele projektet og rydde specifikke ark
function dailyBackupAndClear() {
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();

  // Opret en kopi af hele projektet med dags dato MINUS 1 dag som navn
  var date = new Date();
  date.setDate(date.getDate() - 1); // Træk én dag fra den aktuelle dato
  var dateString = Utilities.formatDate(date, Session.getScriptTimeZone(), "yyyy-MM-dd");
  var copyName = "Maskinstatus (" + dateString + ")";
  var file = DriveApp.getFileById(spreadsheet.getId());
  var copy = file.makeCopy(copyName);

  // Flyt kopien til mappen "maskinstatus logbog"
  var folderId = "1VvjB6pOpwmYn9KbvAb09pCRFleSbwVC7"; // Mappe-ID for "maskinstatus logbog"
  var folder = DriveApp.getFolderById(folderId);
  folder.addFile(copy);
  DriveApp.getRootFolder().removeFile(copy); // Fjern kopien fra rodmappen

  // Ryd indholdet af alle ark, hvor navnet indeholder "_maskinStatus" eller hedder "Unexpected Data"
  var sheets = spreadsheet.getSheets();
  for (var i = 0; i < sheets.length; i++) {
    var sheetName = sheets[i].getName();
    if (sheetName.includes("_maskinStatus") || sheetName === "Unexpected Data") {
      sheets[i].clearContents(); // Ryd indholdet af arket

      // Sæt overskrifterne tilbage for _maskinStatus ark
      if (sheetName.includes("_maskinStatus")) {
        sheets[i].getRange(1, 1).setValue("Timestamp");
        sheets[i].getRange(1, 2).setValue(sheetName + " (10)");
        sheets[i].getRange(1, 3).setValue(sheetName + " (5)");
        sheets[i].getRange(1, 4).setValue(sheetName + " (0)");
      }
    }
  }

  Logger.log("Daglig kopiering og rydning fuldført: " + copyName);
}

function doPost(e) {
  try {
    var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
    var MAX_ROWS = 1000000;
    var HEADER_ROW = 1;
    var TIMESTAMP_COL = 1;

    var properties = PropertiesService.getScriptProperties();
    var processedEventIds = JSON.parse(properties.getProperty("processedEventIds") || "{}");

    var data;
    try {
      data = JSON.parse(e.postData.contents);
    } catch (parseError) {
      logUnexpectedData(e.postData.contents, "JSON Parse Error: " + parseError.toString());
      return ContentService.createTextOutput("JSON Parse Error: " + parseError.toString());
    }

    var values = data.values;

    if (!values || values.length === 0) {
      logUnexpectedData(e.postData.contents, "No data received");
      return ContentService.createTextOutput("No data received");
    }

    var eventId = data.event_id;
    if (processedEventIds[eventId]) {
      logUnexpectedData(e.postData.contents, "Message discarded (duplicate)");
      return ContentService.createTextOutput("Message discarded (duplicate)");
    }

    processedEventIds[eventId] = new Date().getTime();
    properties.setProperty("processedEventIds", JSON.stringify(processedEventIds));

    cleanupProcessedEventIds(processedEventIds, properties);

    var maskinStatusVars = ['c81_maskinStatus', 'c85_maskinStatus', 'd73_maskinStatus', 'd78_maskinStatus', 'i79_maskinStatus', 'i80_maskinStatus', 'i84_maskinStatus'];

    var maskinStatuses = values.filter(v => maskinStatusVars.includes(v.name));

    if (maskinStatuses.length === 0) {
      logUnexpectedData(e.postData.contents, "No matching _maskinStatus values found");
      return ContentService.createTextOutput("No matching _maskinStatus values found");
    }

    var updatedAtUTC = new Date(values[0].updated_at);
    var nowUTC = new Date();

    if (nowUTC - updatedAtUTC > 30 * 1000) {
      logUnexpectedData(e.postData.contents, "Message discarded (too late)");
    }

    maskinStatuses.forEach(maskinStatus => {
      var variableName = maskinStatus.name;
      var sheet = spreadsheet.getSheetByName(variableName);
      if (!sheet) {
        sheet = spreadsheet.insertSheet(variableName);
        sheet.getRange(HEADER_ROW, TIMESTAMP_COL).setValue('Timestamp');
        sheet.getRange(HEADER_ROW, TIMESTAMP_COL + 1).setValue(variableName + ' (10)');
        sheet.getRange(HEADER_ROW, TIMESTAMP_COL + 2).setValue(variableName + ' (5)');
        sheet.getRange(HEADER_ROW, TIMESTAMP_COL + 3).setValue(variableName + ' (0)');
      }

      var value = maskinStatus.value;
      var column;
      if (value === 10) {
        column = TIMESTAMP_COL + 1;
      } else if (value === 5) {
        column = TIMESTAMP_COL + 2;
      } else if (value === 0) {
        column = TIMESTAMP_COL + 3;
      } else {
        logUnexpectedData(e.postData.contents, "Invalid value: " + value + " for " + variableName);
        return;
      }

      if (sheet.getLastRow() >= MAX_ROWS + HEADER_ROW) {
        sheet.deleteRow(MAX_ROWS + HEADER_ROW);
      }

      var lastRow = sheet.getLastRow() + 1;
      var timeString = Utilities.formatDate(updatedAtUTC, Session.getScriptTimeZone(), "HH:mm");
      sheet.getRange(lastRow, TIMESTAMP_COL).setValue(timeString);
      sheet.getRange(lastRow, column).setValue(value);
    });

    logSuccessfulData(e.postData.contents, eventId);

    return ContentService.createTextOutput("Data processed successfully");

  } catch (error) {
    logUnexpectedData(e ? e.postData.contents : "No post data", "Unexpected Error: " + error.toString());
    return ContentService.createTextOutput("Unexpected Error: " + error.toString());
  }
}

function logSuccessfulData(rawData, eventId) {
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = spreadsheet.getSheetByName("Unexpected Data");

  if (!sheet) {
    sheet = spreadsheet.insertSheet("Unexpected Data");
    sheet.appendRow(["Timestamp", "Status", "Event ID", "Raw Data"]);
  }

  var timestamp = new Date();
  sheet.appendRow([timestamp, "Success", eventId, rawData]);

  var MAX_UNEXPECTED_ROWS = 100000;
  if (sheet.getLastRow() > MAX_UNEXPECTED_ROWS) {
    sheet.deleteRow(2);
  }
}

function logUnexpectedData(rawData, errorMessage) {
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = spreadsheet.getSheetByName("Unexpected Data");

  if (!sheet) {
    sheet = spreadsheet.insertSheet("Unexpected Data");
    sheet.appendRow(["Timestamp", "Status", "Event ID", "Raw Data"]);
  }

  var timestamp = new Date();
  sheet.appendRow([timestamp, "Error: " + errorMessage, "", rawData]);

  var MAX_UNEXPECTED_ROWS = 100000;
  if (sheet.getLastRow() > MAX_UNEXPECTED_ROWS) {
    sheet.deleteRow(2);
  }
}

function cleanupProcessedEventIds(processedEventIds, properties) {
  var now = new Date().getTime();
  for (var eventId in processedEventIds) {
    if (now - processedEventIds[eventId] > 60 * 60 * 1000) {
      delete processedEventIds[eventId];
    }
  }
  properties.setProperty("processedEventIds", JSON.stringify(processedEventIds));
}

This is my Arduino Code:

/* 
  Sketch generated by the Arduino IoT Cloud Thing "Untitled"
  https://create.arduino.cc/cloud/things/5ece6a0e-3cc6-4c10-8c6d-3cd3f8b3b7cb 

  Arduino IoT Cloud Variables description

  The following variables are automatically generated and updated when changes are made to the Thing

  String c81_driftBesked;
  float c81_hydraulikTemperatur;
  float c81_luftTemperatur;
  int c81_maskinStatus;
  int c81_relativFugtighed;
  int cyklusTid;
  int kommando;
  int sekunder;
  bool reset;

  Variables which are marked as READ/WRITE in the Cloud Thing will also have functions
  which are called when their values are changed from the Dashboard.
  These functions are generated with the Thing and added at the end of this sketch.
*/


const int ledIdle = 25;
const int ledRun = 26;
const int ledStop = 27;

#define Watchdog 0
#define MaskinNumber "C81 "
#define MaskinCali "TAGER TID"
#define MaskinRun "KØRER"
#define MaskinIdle "VENTER"
#define MaskinError "FEJL"
#define MaskinHidden "   "

#include "thingProperties.h"
#include "OneWire.h"
#include "DallasTemperature.h"

//Data pin. Husk 4.7kohm pullup modstand imellem Data og VCC
#define ONE_WIRE_BUS 23

OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);

//ESP32 Pins 21=SDA 22=SCL
#include <Adafruit_AHTX0.h>

Adafruit_AHTX0 aht;


#define trButton 19      // training/reset button (being eliminated/modified)
#define processInput 19  // needs the sensor from the process
#define faultLED 3
#define readyLED 4
#define runningLED 5

bool needCalibration;  // set this to force a calibration run

enum bar { IDLE = 0,
           MEASURE,
           CHECK,
           IFAULT,
           NFAULT,
};

/* Declaring a global variabl for sensor data
int c81_maskinStatus = 0;
float c81_luftTemperatur = 0;
int c81_relativFugtighed = 0;
float c81_hydraulikTemperatur = 0;
*/



void setup() {
  
     // Define LED pins
  pinMode(readyLED, OUTPUT);
  pinMode(runningLED, OUTPUT);
  pinMode(faultLED, OUTPUT);
  pinMode(trButton, INPUT_PULLUP);
  pinMode(processInput, INPUT_PULLUP);
  pinMode(ledIdle, OUTPUT);
  pinMode(ledRun, OUTPUT);
  pinMode(ledStop, OUTPUT);

  needCalibration = true;
  
  // Initialize serial and wait for port to open:
  Serial.begin(9600);
  // This delay gives the chance to wait for a Serial Monitor without blocking if none is found
  delay(1500); 

  aht.begin();
  sensors.begin();

  pinMode(12, OUTPUT);


  // Defined in thingProperties.h
  initProperties();

  // Connect to Arduino IoT Cloud
  ArduinoCloud.begin(ArduinoIoTPreferredConnection);
  
  /*
     The following function allows you to obtain more information
     related to the state of network and IoT Cloud connection and errors
     the higher number the more granular information you’ll get.
     The default is 0 (only errors).
     Maximum is 4
 */
  setDebugMessageLevel(2);
  ArduinoCloud.printDebugInfo();
}

unsigned int counter;
unsigned char state = IDLE;  // does. is.
unsigned long now;
unsigned long timeLimit;
unsigned long timeLimit1;
unsigned long timer1;
unsigned long previousMillis;
int startTime;
long timerMode;

void loop() {
  ArduinoCloud.update();
  // Your code here 
  
  if (watchdog == 0){
  sekunder = 0;
  c81_maskinStatus = 0;
  c81_driftBesked = MaskinNumber MaskinHidden;
}

    if (now - previousMillis > 1800000) {
    needCalibration = true;
    previousMillis = now;
        }
  
  cyklusTid = (timeLimit / 1000);

  
      now = millis();

  bool controlX = digitalRead(processInput) == HIGH;
  bool buttonState = digitalRead(trButton) == HIGH;

  if(controlX == HIGH || buttonState == HIGH){
  sekunder = ((now - startTime) / 1000.0) + 0.5;

    if(sekunder > 7200) {
      sekunder = 0;
   
    }
  }
  
  else {
    startTime = now;
    sekunder = 0;
}

  static bool lastButtonState;
  static bool buttonPress;
  
  if (buttonState != lastButtonState) {
    if (buttonState) {
      buttonPress = !buttonPress;
    }
    lastButtonState = buttonState;
  }

  switch (state) {
    case IDLE:
      c81_maskinStatus = 5;
      c81_driftBesked = MaskinNumber MaskinIdle;
      digitalWrite(readyLED, HIGH);
      digitalWrite(runningLED, LOW);
      if (controlX) {
        timer1 = now;
        //      if (buttonPress || needCalibration) {
        if (needCalibration) {
          state = MEASURE;
          Serial.print("training for time\n");
          buttonPress = false;
          needCalibration = false;
        } else {
          state = CHECK;
          Serial.print("X up. monitoring time\n");
        }
      }
      break;

    case CHECK:
      c81_maskinStatus = 10;
      c81_driftBesked = MaskinNumber MaskinRun;
      digitalWrite(readyLED, LOW);
      digitalWrite(runningLED, HIGH);
      if (now - timer1 > timeLimit1) {
        state = IFAULT;
        break;
      }
      if (!controlX) {
        Serial.print("X down     made it by ");
        Serial.println(timeLimit - (now - timer1));
        

        state = IDLE;
      }
      break;

    case MEASURE:
      c81_maskinStatus = 10;
      c81_driftBesked = MaskinNumber MaskinCali;
      digitalWrite(readyLED, LOW);
      digitalWrite(runningLED, HIGH);
      if (now - timer1 > 1000000){
        state = IFAULT;
        break;
      }
      if (!controlX) {
        timeLimit = now - timer1 + 1000;
        timeLimit1 = timeLimit * 1.10;  // comfort margarine 10 percent here
        Serial.print("X down    new period = ");
        Serial.println(timeLimit);

        state = IDLE;
      }
      break;

    case IFAULT:
      Serial.print(" initialize fault mechanism");
      state = NFAULT;
      Serial.print(" / perpetuate fault mechanism\n");
      c81_maskinStatus = 0;
      c81_driftBesked = MaskinNumber MaskinError;
      digitalWrite(faultLED, HIGH);
      buttonPress = false;  // eat any stray button presses
      break;

    case NFAULT:
      digitalWrite(readyLED, LOW);
      digitalWrite(runningLED, LOW);
      digitalWrite(faultLED, millis() & 512 ? HIGH : LOW);
      if (buttonPress) {
        Serial.print("sytem to IDLE\n");
        digitalWrite(faultLED, LOW);
        buttonPress = false;
        needCalibration = true;
        state = IDLE;
      }
      break;
}

  sensors_event_t humidity, temp;
  aht.getEvent(&humidity, &temp);// populate temp and humidity objects with fresh data

  c81_luftTemperatur = temp.temperature;
  c81_relativFugtighed = humidity.relative_humidity;
  sensors.requestTemperatures();
  c81_hydraulikTemperatur = sensors.getTempCByIndex(0);

}

/*
  Since Kommando is READ_WRITE variable, onKommandoChange() is
  executed every time a new value is received from IoT Cloud.
*/
void onKommandoChange()  {
  // Add your code here to act upon Kommando change
  if (kommando == 1){
    digitalWrite(ledIdle, LOW);
    digitalWrite(ledRun, HIGH);
    digitalWrite(ledStop, LOW);
      }
    if (kommando == 2){
    digitalWrite(ledIdle, HIGH);
    digitalWrite(ledRun, LOW);
    digitalWrite(ledStop, LOW);
      }
      if (kommando == 3){
    digitalWrite(ledIdle, LOW);
    digitalWrite(ledRun, LOW);
    digitalWrite(ledStop, HIGH);
      }
}

/*
  Since Reset is READ_WRITE variable, onResetChange() is
  executed every time a new value is received from IoT Cloud.
*/
void onResetChange()  {
  // Add your code here to act upon Reset change
  if (reset == true) {pinMode(9, OUTPUT);}
}



/*
  Since CyklusTid is READ_WRITE variable, onCyklusTidChange() is
  executed every time a new value is received from IoT Cloud.
*/
void onCyklusTidChange()  {
  // Add your code here to act upon CyklusTid change
  
}

thingProperties:

// Code generated by Arduino IoT Cloud, DO NOT EDIT.

#include <ArduinoIoTCloud.h>
#include <Arduino_ConnectionHandler.h>

const char DEVICE_LOGIN_NAME[]  = "****************";

const char SSID[]               = SECRET_SSID;    // Network SSID (name)
const char PASS[]               = SECRET_OPTIONAL_PASS;    // Network password (use for WPA, or use as key for WEP)
const char DEVICE_KEY[]  = SECRET_DEVICE_KEY;    // Secret device password

void onKommandoChange();
void onResetChange();

String c81_driftBesked;
float c81_hydraulikTemperatur;
float c81_luftTemperatur;
int c81_maskinStatus;
int c81_relativFugtighed;
int cyklusTid;
int kommando;
int sekunder;
bool reset;

void initProperties(){

  ArduinoCloud.setBoardId(DEVICE_LOGIN_NAME);
  ArduinoCloud.setSecretDeviceKey(DEVICE_KEY);
  ArduinoCloud.addProperty(c81_driftBesked, READ, ON_CHANGE, NULL);
  ArduinoCloud.addProperty(c81_hydraulikTemperatur, READ, 60 * SECONDS, NULL);
  ArduinoCloud.addProperty(c81_luftTemperatur, READ, 60 * SECONDS, NULL);
  ArduinoCloud.addProperty(c81_maskinStatus, READ, 30 * SECONDS, NULL);
  ArduinoCloud.addProperty(c81_relativFugtighed, READ, 60 * SECONDS, NULL);
  ArduinoCloud.addProperty(cyklusTid, READ, 60 * SECONDS, NULL);
  ArduinoCloud.addProperty(kommando, READWRITE, ON_CHANGE, onKommandoChange);
  ArduinoCloud.addProperty(sekunder, READ, 30 * SECONDS, NULL);
  ArduinoCloud.addProperty(reset, READWRITE, ON_CHANGE, onResetChange);

}

WiFiConnectionHandler ArduinoIoTPreferredConnection(SSID, PASS);

Hi!
Webhook works per thing, not for a single property. Maybe you can use "Sync with another property". You can have a different thing containing only the variable you want in Webhook and sync that property with the original one.

Good idea, I have tested it but it doesn't seem to work.

I created a thing with the exact same name and variable and synced the variable.

The data syncs up properly between the things but the webhook doesn't seem to update and send the data.

I might need to associate a device to it?

Yes, try to connect a device to the newly created Thing.

I'm in the process of dealing with something similar and this may help.

In Google Script, you can do a loop through the variables and continue until you find the one you want.

For example, I have a 'message' IOT Cloud variable that is a string and updates on change on an OPTA. The code below reads the webhook defined on the Thing and picks message for doing other things, ignoring the other variables that are updated:

var cloudData = JSON.parse(e.postData.contents);
var thingData = cloudData.values;

for( var i=0; i < thingData.length; i++ ) {
    if( thingData[i].name != 'message' ) { continue; }
    var mString = thingData[i].value;
    ...
}

I caught your post though based on the title "Dublicated"... Do you mean "Duplicated"?

The code above does execute duplicates at times for some devices but not for others, even though the state of "message" hasn't changed.

In doing more with webhooks, the only data that is pushed from IOT Cloud to the Google Script are variables that change state. Thus, the JSON file does not have values for EVERY variable, but only the ones that have been updated, suposedly. But I do see duplicates from time-to-time even with the same 'updated_at' timestamp.

I'm wondering why?

I checked my Active Google Scripts deployments, and there are not two versions running.

I'm suspecting it may be a timing issue. You state 600 seconds which seems too long.

Thank you for any input, and if I find my error, I'll post back.

Chris

Yes I meant duplicated.

Anyway what I experience is if I have like 10 variables in a "Thing", then a JSON file containing all the variables, will be send every time one of the variables updates.
So if I set the update interval on everything variable to like 5 second, then what will happen is every 5 seconds there will be send 10 identical JSON files.

Therefore I needed to use something like 600 seconds on the variables I don't need in my Google sheet, because then it will only be every 600 seconds it sends 10 JSON files at the same time making me stay within the allowed amount errors.

Yeah, I have the same problem!
Here is my script, post yours as well:

/*
* Copyright 2022 Peter Bendel
* copied (and then modified) from: https://github.com/arduino/arduino-iot-google-sheet-script/blob/master/Code.gs
* Copyright 2018 ARDUINO SA (http://www.arduino.cc/)
* This file is part of arduino-iot-google-sheet-script.
* Copyright (c) 2019
* Authors: Marco Passarello
*
* This software is released under:
* The GNU General Public License, which covers the main part of 
* arduino-iot-google-sheet-script
* The terms of this license can be found at:
* https://www.gnu.org/licenses/gpl-3.0.en.html
*
* You can be released from the requirements of the above licenses by purchasing
* a commercial license. Buying such a license is mandatory if you want to modify or
* otherwise use the software for commercial activities involving the Arduino
* software without disclosing the source code of your own applications. To purchase
* a commercial license, send an email to license@arduino.cc.
*
*/


// get active spreadsheet
var ss = SpreadsheetApp.getActiveSpreadsheet();

// get sheet named ArduinoIOTCloudSensorData
var sheet = ss.getSheetByName('RawData');


var MAX_ROWS = 1440;     // max number of data rows to display
// 1 day * 60 (per hour) * 24 hours
var HEADER_ROW = 1;     // row index of header
var TIMESTAMP_COL = 1;  // column index of the timestamp column
var INTERVAL_IN_SECONDS = 60;  // 1 minute

function doGet(e) {
  return ContentService.createTextOutput("Webhook is up.");
}

function doPost(e) {  
  var cloudData = JSON.parse(e.postData.contents); // this is a json object containing all info coming from IoT Cloud
  //var webhook_id = cloudData.webhook_id; // really not using these three
  //var device_id = cloudData.device_id;
  //var thing_id = cloudData.thing_id;
  var values = cloudData.values; // this is an array of json objects
  
  // store names and values from the values array
  // just for simplicity
  var incLength = values.length;
  var incNames = [];
  var incValues = [];
  for (var i = 0; i < incLength; i++) {
    incNames[i] = values[i].name;
    incValues[i] = values[i].value;
  }
  
  // read timestamp of incoming message
  var timestamp = values[0].updated_at;          // format: yyyy-MM-ddTHH:mm:ss.mmmZ
  var date = new Date(Date.parse(timestamp)); 
    
    // this section write property names 
    sheet.getRange(HEADER_ROW, 1).setValue('timestamp');
    for (var i = 0; i < incLength; i++) {
      var lastCol = sheet.getLastColumn(); // at the very beginning this should return 1 // second cycle -> it is 2
      if (lastCol == 1) {
        sheet.getRange(HEADER_ROW, lastCol + 1).setValue(incNames[i]);
      } else {
        // check if the name is already in header
        var found = 0;
        for (var col = 2; col <= lastCol; col++) {
          if (sheet.getRange(HEADER_ROW, col).getValue() == incNames[i]) {
            found = 1;
            break;
          }
        }
        if (found == 0) {
          sheet.getRange(HEADER_ROW, lastCol+1).setValue(incNames[i]);
        }
      }
    }
    
    // redefine last coloumn and last row since new names could have been added
    var lastCol = sheet.getLastColumn();
    var lastRow = sheet.getLastRow();
    
    // delete last row to maintain constant the total number of rows
    if (lastRow > MAX_ROWS + HEADER_ROW - 1) { 
      sheet.deleteRow(lastRow);
    }
  
  if (lastRow > 1) { // this is not the first time - check if we have a new date
    // read last date written and only if we are at least 
    var lastDate = sheet.getRange(HEADER_ROW+1, TIMESTAMP_COL).getValue();
    // only if enough time has passed after last reporting interval add a new row
    if (date.getTime() - lastDate.getTime() > (INTERVAL_IN_SECONDS*1000)) { 
      // insert next row after header row 
      sheet.insertRowAfter(HEADER_ROW);
      // write the timestamp only once per interval
      sheet.getRange(HEADER_ROW+1, TIMESTAMP_COL).setValue(date).setNumberFormat("yyyy-MM-dd HH:mm:ss");
    }
  } else {
    // insert first row after header row 
    sheet.insertRowAfter(HEADER_ROW);
    // write the timestamp only once per interval
    sheet.getRange(HEADER_ROW+1, TIMESTAMP_COL).setValue(date).setNumberFormat("yyyy-MM-dd HH:mm:ss");
  }
    
    // reset style of the new row, otherwise it will inherit the style of the header row
    var range = sheet.getRange('A2:Z2');
    //range.setBackground('#ffffff');
    range.setFontColor('#000000');
    range.setFontSize(10);
    range.setFontWeight('normal');
    
    // write values in the respective columns
    for (var col = 1+TIMESTAMP_COL; col <= lastCol; col++) {
      for (var i = 0; i < incLength; i++) {
        var currentName = sheet.getRange(HEADER_ROW, col).getValue();
        if (currentName == incNames[i]) {
          // turn boolean values into 0/1, otherwise google sheets interprets them as labels in the graph
          if (incValues[i] == true) {
            incValues[i] = 1;
          } else if (incValues[i] == false) {
            incValues[i] = 0;
          }
          sheet.getRange(HEADER_ROW+1, col).setValue(incValues[i]);
        } 
      }
    }
    return ContentService.createTextOutput("OK");  
}

I found the problem and it has nothing to do with the script but with how Arduino cloud works.

Look at this post: Webhook becomes inactive after some time - #10 by henrik9979

1 Like

So, how do I fix this?

I had an instance in the past week where (3) Things using the same webhook posting to Google Script, and then onto my local server, stopped sending data, but only for (2) of the (3) Things.

Same code as I posted below in this thread:

var cloudData = JSON.parse(e.postData.contents);
var thingData = cloudData.values;

for( var i=0; i < thingData.length; i++ ) {
    if( thingData[i].name != 'message' ) { continue; }
    var mString = thingData[i].value;
    ...
}

Sure enough, the webhook was showing 'inactive' for these (2) Things, and 'active' for the 3rd Things. I was able to reset them by going into the Things, clicking 'Change' then 'Set Webhook' in the pop-up. Tedious, but manageable for (2) Things.

The cause was probably between Arduino IOT Cloud and Google, as there is nothing that sends a 4/5 error in the Google Script if there's a fault between Google and my server. At least not that I'm aware of.

Also, there isn't anything that I can find in the IOT Cloud API that checks for Webhook status which would be nice, and bonus points if the API could reset it!

The data coming from the Arduino device via the webhook is not periodic, but dependent on external environment.

The work-around, is to set a periodic variable in the Thing

int webhookTestPeriod;
void initProperties(){
  ArduinoCloud.addProperty(webhookTestPeriod, READ, 60 * SECONDS, NULL);
}

/****/

void loop() {
  /****/

  webhookTestPeriod = 1;

  /****/
}

have the webhook send it to your third party, Google in my case, check if the period matches and if not, send a message (there are several ways to do this) to check the webhook status. This again, requires a manual check, which could be tedious... but at least you know rather than guess.

Hi @macolomb

Do you know of any development or support in the IOT Cloud API to interface with the webhooks?

Thank you.

The problem is if you have multiple parameters inside the same Thing and you make a web hook, what is going to happen is even though you only use one of the parameters, every time one of the parameters updates it sends a new json file with all the data from each parameter to Google Sheets.

So if you have like 5 parameters that updates every second, you end up sending 5 json files pr. second resulting in an spam protection getting tricked and the web hook becomes inactive.

And if you add some confirmation function in your Google script which checks for dublicates and incorrect time stamps, that will send a message back to Arduino Cloud telling it there was an error, you problem becomes even worse. Arduino Cloud doesn't allow data being send back to webhooks, also resulting in inactive webhooks.

What I did was:

  1. Removing anything in my Google script that sends any data to Arduino Cloud.
  2. Set the update interval on parameters I did use in Google sheet to something high like 5-10 minutes.
  3. The parameters that did use was set to different intervals to minimise double sendings I used 30 sekunds and 60 seconds.

The last thing I considered was to create a new Thing that had parameters that was synchronising with the parameters from other things and connect a webhooks in that. That way I could have realtime data on all my parameters without it sending any webhooks then only created 1 parameters that had an update interval of 10 second.

Don't know if that make sense what I'm telling you?

I've actually been thinking more about the method proposed by @macolomb: making a separate Thing with selected variables from other Things through the sync feature that the IOT Cloud provides, and then using a single webhook on this Thing.

What I'm observing now is that Things using the same webhook to pull the same variable 'name' and 'function' across multiple Things, are failing more often causing the webhook to go 'inactive'.

In using Google Scripts, I can see in real-time that Google is executing the script often: several times per second in some cases.

This has to do with sensor data definitions in different Things updating in the 5s to 15s interval. Every time there's a changed state in a Thing with an active webhook, the webhook executes.

We don't care much about the 5-15s interval data webhook, instead prefering these for timeseries analysis in the back-end. For the webhook, there's a higher importance of 'status' type variables that update 'on change' usually once per day or week.

So, I'll play around with making a separate Thing, containing a sync to these less frequent variables of higher importance, and then directing our webhook to this single Thing in hopes of greater reliability in not going 'inactive'.

The other two options other than a webhook is to 1) make calls through the API. But this is a call, call-back action which seems like more overhead. Or, 2) set up triggers in the IOT Cloud, direct the messages to a single email, and then have an email API in the back-end.

To have the initial method of a single webhook associated to multiple Things relaying through Google Script work for several months to suddenly cause these webhooks to go 'inactive' within days implies to me that performance / policy / coding between IOT Cloud and Google, or other approved vendors, have changed. So it is... still very pleased with the IOT Cloud though. Keep up the good work!

You are absolutely correct in having X >= 5 parameters updating in 1-5s intervals across multiple Things tied to one webhook sending a LOT of .json data across the net. Even one variable updating in 1-5s in each Thing will do it.

I am not sure about any spam flags, at least from Google Script. I don't see any evidence of it, but just because I don't see it doesn't mean it's not true :slight_smile: When the webhook goes 'inactive' in a Thing, I just click the webhook setting to reset it. I don't even change the link string. I would think that a spam flag might try to block more than the same request, perhaps the whole originating domain!

I like the approach of not sending any notice back to IOT Cloud. My Google Scripts have an error logging feature, and this may be doing a callback to IOT Cloud which may cause the webhook going 'inactive'. I'll take a look...

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.