Client API

Temp Stick API Documentation

Base URL https://tempstickapi.com/api/v1

Getting Started

The Temp Stick API is intended for developers who are comfortable making HTTP requests, working with JSON responses, and handling common API client behavior such as headers, authentication, compression, and form data.

Base URL#

https://tempstickapi.com/api/v1

Authentication#

Every request must include your account API key in the X-API-Key HTTP header.

X-API-Key: YOUR_API_KEY

You can find your API key in your Temp Stick account:

1. Log in to your Temp Stick account.

2. Go to the Account Page.

3. Select the Developers tab.

4. Click Show Key to reveal your API key.

All API clients should identify themselves clearly on every request. Do not send bare minimum requests that only include the API key.

At minimum, include these headers with every request:

X-API-Key: YOUR_API_KEY
Accept: application/json
User-Agent: YourAppName/1.0
X-Client-Name: YourAppName
X-Client-Version: 1.0

Recommended header details:

Header Required Description
X-API-Key Yes Your Temp Stick account API key.
Accept Recommended Use application/json.
User-Agent Recommended A descriptive application name and version. Avoid generic defaults such as curl, python-requests, or unnamed HTTP clients in production.
X-Client-Name Recommended The name of your application, service, integration, or company.
X-Client-Version Recommended The version of your application or integration.
Content-Type Required for POST Use multipart/form-data for POST requests that update settings.
Accept-Encoding Optional Your HTTP client may include this automatically. The API may return gzip-compressed data.

Example header set#

headers = {
    "X-API-Key": "YOUR_API_KEY",
    "Accept": "application/json",
    "User-Agent": "MyTempStickApp/1.0",
    "X-Client-Name": "MyTempStickApp",
    "X-Client-Version": "1.0",
}

Avoid requests that only send the API key and no client identity information:

headers = {
    "X-API-Key": "YOUR_API_KEY",
    "Accept": "application/json",
}

Compression#

API responses may be compressed with gzip and returned as JSON.

Most modern HTTP clients automatically decompress gzip responses when configured correctly. Make sure your application supports compressed responses before parsing the JSON.

Temperature Units#

All temperature readings and temperature settings are returned in Celsius.

To convert Celsius to Fahrenheit:

(CELSIUS * 1.8) + 32

PHP conversion helper#

function celsius_to_fahrenheit($celsius, $round = 1) {
    return round(($celsius * 1.8) + 32, $round);
}

Dates and Times#

All timestamps returned by the API use the UTC time zone.

UTC timestamps may be marked with Z or with the offset +00:00.

Examples:

2026-04-27T15:30:00Z
2026-04-27T15:30:00+00:00

Convert timestamps to the user's local time zone in your application when displaying dates and times.

GET Requests#

Use GET requests to retrieve data from the API.

GET requests should:

  • Include all recommended identifying headers.
  • Not include a request body.
  • Use Accept: application/json.
  • Use a descriptive User-Agent.

PHP GET example#

<?php

$curl = curl_init();

curl_setopt_array($curl, [
    CURLOPT_URL => 'https://tempstickapi.com/api/v1/sensors/all',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_ENCODING => '', // Automatically decode gzip responses.
    CURLOPT_TIMEOUT => 30,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_HTTPHEADER => [
        'X-API-Key: YOUR_API_KEY',
        'Accept: application/json',
        'User-Agent: MyTempStickApp/1.0',
        'X-Client-Name: MyTempStickApp',
        'X-Client-Version: 1.0',
    ],
]);

$response = curl_exec($curl);

if ($response === false) {
    $error = curl_error($curl);
    curl_close($curl);
    throw new RuntimeException('Temp Stick API request failed: ' . $error);
}

$http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);

if ($http_code < 200 || $http_code >= 300) {
    throw new RuntimeException('Temp Stick API returned HTTP ' . $http_code . ': ' . $response);
}

echo $response;

?>

JavaScript GET example#

const headers = new Headers();

headers.append("X-API-Key", "YOUR_API_KEY");
headers.append("Accept", "application/json");
headers.append("User-Agent", "MyTempStickApp/1.0");
headers.append("X-Client-Name", "MyTempStickApp");
headers.append("X-Client-Version", "1.0");

const requestOptions = {
  method: "GET",
  headers: headers,
  redirect: "follow"
};

fetch("https://tempstickapi.com/api/v1/sensors/all", requestOptions)
  .then(response => {
    if (!response.ok) {
      throw new Error(`Temp Stick API returned HTTP ${response.status}`);
    }

    return response.json();
  })
  .then(result => console.log(result))
  .catch(error => console.error("Temp Stick API request failed:", error));

> Note: Browser-based JavaScript may restrict setting certain headers, including User-Agent. For production integrations, server-side requests are recommended when you need full control over request headers.

cURL GET example#

curl --location --request GET 'https://tempstickapi.com/api/v1/sensors/all' \
  --header 'X-API-Key: YOUR_API_KEY' \
  --header 'Accept: application/json' \
  --header 'User-Agent: MyTempStickApp/1.0' \
  --header 'X-Client-Name: MyTempStickApp' \
  --header 'X-Client-Version: 1.0'

Python GET example#

import requests

url = "https://tempstickapi.com/api/v1/sensors/all"

headers = {
    "X-API-Key": "YOUR_API_KEY",
    "Accept": "application/json",
    "User-Agent": "MyTempStickApp/1.0",
    "X-Client-Name": "MyTempStickApp",
    "X-Client-Version": "1.0",
}

response = requests.get(url, headers=headers, timeout=30)
response.raise_for_status()

data = response.json()
print(data)

POST Requests#

Use POST requests when updating sensor settings.

POST requests should:

  • Include all recommended identifying headers.
  • Send data as multipart/form-data.
  • Use field names exactly as documented for the endpoint.
  • Send only supported fields for the endpoint you are calling.

When using libraries such as PHP cURL, Python requests, or JavaScript FormData, the client usually creates the correct multipart/form-data boundary automatically. Do not manually set the boundary unless your HTTP client requires it.

PHP POST example#

<?php

$curl = curl_init();

curl_setopt_array($curl, [
    CURLOPT_URL => 'https://tempstickapi.com/api/v1/sensor/YOUR_SENSOR_ID',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_ENCODING => '',
    CURLOPT_TIMEOUT => 30,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_POSTFIELDS => [
        'sensor_name' => 'Test',
        'send_interval' => '3600',
        'alert_interval' => '1800',
        'use_alert_interval' => '0',
        'alert_temp_below' => '',
        'alert_temp_above' => '',
        'alert_humidity_below' => '',
        'alert_humidity_above' => '',
        'connection_sensitivity' => '3',
        'use_offset' => '0',
        'temp_offset' => '0',
        'humidity_offset' => '0',
    ],
    CURLOPT_HTTPHEADER => [
        'X-API-Key: YOUR_API_KEY',
        'Accept: application/json',
        'User-Agent: MyTempStickApp/1.0',
        'X-Client-Name: MyTempStickApp',
        'X-Client-Version: 1.0',
    ],
]);

$response = curl_exec($curl);

if ($response === false) {
    $error = curl_error($curl);
    curl_close($curl);
    throw new RuntimeException('Temp Stick API request failed: ' . $error);
}

$http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);

if ($http_code < 200 || $http_code >= 300) {
    throw new RuntimeException('Temp Stick API returned HTTP ' . $http_code . ': ' . $response);
}

echo $response;

?>

JavaScript POST example#

const formData = new FormData();

formData.append("sensor_name", "Test");
formData.append("send_interval", "3600");
formData.append("alert_interval", "1800");
formData.append("use_alert_interval", "0");
formData.append("alert_temp_below", "");
formData.append("alert_temp_above", "");
formData.append("alert_humidity_below", "");
formData.append("alert_humidity_above", "");
formData.append("connection_sensitivity", "3");
formData.append("use_offset", "0");
formData.append("temp_offset", "0");
formData.append("humidity_offset", "0");

const headers = new Headers();

headers.append("X-API-Key", "YOUR_API_KEY");
headers.append("Accept", "application/json");
headers.append("User-Agent", "MyTempStickApp/1.0");
headers.append("X-Client-Name", "MyTempStickApp");
headers.append("X-Client-Version", "1.0");

const requestOptions = {
  method: "POST",
  headers: headers,
  body: formData,
  redirect: "follow"
};

fetch("https://tempstickapi.com/api/v1/sensor/YOUR_SENSOR_ID", requestOptions)
  .then(response => {
    if (!response.ok) {
      throw new Error(`Temp Stick API returned HTTP ${response.status}`);
    }

    return response.json();
  })
  .then(result => console.log(result))
  .catch(error => console.error("Temp Stick API request failed:", error));

> Note: When using FormData, do not manually set the Content-Type header in browser JavaScript. The browser will set multipart/form-data with the correct boundary.

cURL POST example#

curl --location --request POST 'https://tempstickapi.com/api/v1/sensor/YOUR_SENSOR_ID' \
  --header 'X-API-Key: YOUR_API_KEY' \
  --header 'Accept: application/json' \
  --header 'User-Agent: MyTempStickApp/1.0' \
  --header 'X-Client-Name: MyTempStickApp' \
  --header 'X-Client-Version: 1.0' \
  --form 'sensor_name=Test' \
  --form 'send_interval=3600' \
  --form 'alert_interval=1800' \
  --form 'use_alert_interval=0' \
  --form 'alert_temp_below=' \
  --form 'alert_temp_above=' \
  --form 'alert_humidity_below=' \
  --form 'alert_humidity_above=' \
  --form 'connection_sensitivity=3' \
  --form 'use_offset=0' \
  --form 'temp_offset=0' \
  --form 'humidity_offset=0'

Python POST example#

import requests

url = "https://tempstickapi.com/api/v1/sensor/YOUR_SENSOR_ID"

headers = {
    "X-API-Key": "YOUR_API_KEY",
    "Accept": "application/json",
    "User-Agent": "MyTempStickApp/1.0",
    "X-Client-Name": "MyTempStickApp",
    "X-Client-Version": "1.0",
}

data = {
    "sensor_name": "Test",
    "send_interval": "3600",
    "alert_interval": "1800",
    "use_alert_interval": "0",
    "alert_temp_below": "",
    "alert_temp_above": "",
    "alert_humidity_below": "",
    "alert_humidity_above": "",
    "connection_sensitivity": "3",
    "use_offset": "0",
    "temp_offset": "0",
    "humidity_offset": "0",
}

response = requests.post(url, headers=headers, files={}, data=data, timeout=30)
response.raise_for_status()

result = response.json()
print(result)

Common Issues#

406 Not Acceptable#

A 406 Not Acceptable response usually means the request format or headers were rejected before the API request could be processed.

Common causes include:

  • Using a generic or default User-Agent.
  • Sending a GET request with a request body.
  • Sending malformed headers.
  • Including blocked or suspicious keywords in headers.
  • Using a client that does not identify the application clearly.

Recommended fixes:

  • Use a descriptive User-Agent, such as MyTempStickApp/1.0.
  • Include X-Client-Name and X-Client-Version.
  • Do not send a body with GET requests.
  • Remove unnecessary headers.
  • Test the same request in Postman or another API client where headers are easy to inspect.

429 Too Many Requests#

A 429 Too Many Requests response means the client is sending too many requests or the request pattern looks automated without enough client identity.

Recommended fixes:

  • Include all recommended identifying headers on every request.
  • Use a descriptive and stable User-Agent.
  • Include X-Client-Name and X-Client-Version.
  • Avoid retry loops without delays.
  • Add exponential backoff when retrying failed requests.
  • Cache data in your own application when appropriate.
  • Avoid polling endpoints more often than needed.

Recommended retry pattern:

First retry: wait 2 seconds
Second retry: wait 5 seconds
Third retry: wait 15 seconds
Additional retries: wait longer, or stop and alert the user

Testing with Postman#

Postman is recommended when testing or troubleshooting the API because it makes request headers, parameters, and responses easy to inspect.

When creating a request in Postman:

1. Set the method to GET or POST.

2. Enter the full API endpoint URL.

3. Add the recommended headers:

  • X-API-Key
  • Accept
  • User-Agent
  • X-Client-Name
  • X-Client-Version

4. For POST requests, use form-data in the Body tab.

5. Send the request and inspect the response body and status code.

Request Checklist#

Before sending a request to the Temp Stick API, confirm that:

  • The request includes X-API-Key.
  • The request includes a descriptive User-Agent.
  • The request includes X-Client-Name.
  • The request includes X-Client-Version.
  • GET requests do not include a request body.
  • POST requests use multipart/form-data.
  • Your client can handle gzip-compressed responses.
  • Your application treats API timestamps as UTC.
  • Your application treats API temperatures as Celsius.

Endpoints

12 endpoints documented from api\v1.

GET /alerts/:alert_id #

Get Alert

Get settings for a single alert

Group
Alerts
Version
1.0.0
Source
api\v1\index.php

Parameters

Field Type Required Description
alert_id Number Required The ID of your alert

Success Examples

Success Response

{
  "type": "success",
  "message": "get alert",
  "data": {
    "id": "alert_id",
    "user_id": "user_id",
    "alert_name": "Connection",
    "reading_type": "connection",
    "reading_condition": "lost",
    "reading_value": "0.00",
    "alert_message": "[sensor_name]: connection [trigger_value]",
    "always_active": "1",
    "sensors_global": "1",
    "enabled": "1",
    "push_notifications": "1",
    "email_text_notifications": "0",
    "connection_sensitivity": "0",
    "sensors": [
      "sensor_id"
    ],
    "contacts": {
      "contact_id": {
        "id": "contact_id",
        "send_email": 1,
        "send_text": 1
      }
    },
    "active_when": {
      "0": {
        "alert_day": "0",
        "alert_time_start": 12,
        "alert_time_end": 24
      },
      "2": {
        "alert_day": "2",
        "alert_time_start": 1,
        "alert_time_end": 13
      }
    }
  }
}
GET /alerts/all #

Get Alerts

Get all your alerts

Group
Alerts
Version
1.0.0
Source
api\v1\index.php

Success Examples

Success Response

{
    "type": "success",
    "message": "get alerts",
    "data": [
      {
        "id": "",
        "user_id": "",
        "alert_name": "Temperature",
        "reading_type": "temperature",
        "reading_condition": "above",
        "reading_value": "7.20",
        "alert_message": "[sensor_name]: [trigger_type] [trigger_condition] [trigger_value] ([sensor_value])",
        "always_active": "1",
        "sensors_global": "1",
        "enabled": "1",
        "active_when": [],
        "contacts": [],
        "sensors": [],
      }
    ]
  }
GET /sensor/notifications/:sensorId #

Get Sensor Notifications

Get the last seven days of alerts generated by the sensor

Group
Alerts
Version
1.0.0
Source
api\v1\index.php

Parameters

Field Type Required Description
sensorId String Required

Success Examples

Success Response

{
  "type": "success",
  "message": "get sensor notifications",
  "data": {
    "items": [
      {
        "id": "",
        "user_id": "",
        "alert_id": "",
        "event_type": "temperature",
        "event_message": "above 45 F (71.8 F)",
        "event_data": "{\"alert_condition\":\"above\",\"alert_value\":\"45 F\",\"reading_value\":\"71.8 F\"}",
        "created": "2022-05-10 22:49:41Z"
      }
    ],
    "count": "350"
  }
}
GET /user/notifications #

Get User Notifications

Get the last seven days of alerts generated by all sensors

Group
Alerts
Version
1.0.0
Source
api\v1\index.php

Success Examples

Success Response

{
  "type": "success",
  "message": "get user notifications",
  "data": {
    "items": [
      {
        "id": "",
        "user_id": "",
        "sensor_id": "",
        "alert_id": "",
        "event_type": "temperature",
        "event_message": "above 45 F (69.6 F)",
        "event_data": "{\"alert_condition\":\"above\",\"alert_value\":\"45 F\",\"reading_value\":\"69.6 F\"}",
        "created": "2022-05-11 17:37:21Z",
        "sensor": {}
      }
    ],
    "count": "350"
  }
}
GET /sensor/:sensor_id #

Get Sensor

Get settings for a specific sensor, *note: readings are shown in Celsius*

Group
Sensors
Version
1.0.0
Source
api\v1\index.php

Parameters

Field Type Required Description
sensor_id String Required The ID of the sensor you want to get readings from

Success Examples

Success Response

{
  "type": "success",
  "message": "get sensor",
  "data": {
    "version": "",
    "sensor_id": "",
    "sensor_name": "",
    "sensor_mac_addr": "",
    "owner_id": "",
    "type": "DHT",
    "alert_interval": "1800",
    "send_interval": "1800",
    "last_temp": 21,
    "last_humidity": 28.26,
    "last_voltage": 3,
    "battery_pct": 100,
    "wifi_connect_time": 1,
    "rssi": -37,
    "last_checkin": "2022-05-12 19:09:41-00:00Z",
    "next_checkin": "2022-05-12 19:39:41-00:00Z",
    "ssid": "",
    "offline": "0",
    "alerts": [],
    "use_sensor_settings": 0,
    "temp_offset": "0",
    "humidity_offset": "0",
    "alert_temp_below": "",
    "alert_temp_above": "",
    "alert_humidity_below": "",
    "alert_humidity_above": "",
    "connection_sensitivity": "3",
    "use_alert_interval": 0,
    "use_offset": "0",
    "last_messages": [
      {
        "temperature": 21,
        "humidity": 28.26,
        "voltage": "3.00",
        "RSSI": "-37",
        "time_to_connect": "1.0",
        "sensor_time_utc": "2022-05-12 19:09:41"
      }
    ]
  }
}
POST /sensor/:sensor_id #

Update Sensor Settings

Group
Sensors
Version
1.0.0
Source
api\v1\index.php

Parameters

Field Type Required Description
sensor_id String Required

Success Examples

Success Example

{
  "type": "success",
  "message": "update sensor",
  "data": {
    "sensor_name": "sensor_name",
    "send_interval": 1800,
    "alert_interval": 1800,
    "connection_sensitivity": "2",
    "use_offset": "1",
    "temp_offset": "1",
    "humidity_offset": "0",
    "use_sensor_settings": "0",
    "use_alert_interval": 0,
    "alert_temp_below": "",
    "alert_temp_above": "",
    "alert_humidity_below": "",
    "alert_humidity_above": ""
  }
}
GET /sensor/:sensor_id/readings #

Get Readings

Get the readings for a sensor given a specified period, *note: readings are shown in Celsius*

Group
Sensors
Version
1.0.0
Source
api\v1\index.php

Parameters

Field Type Required Description
sensor_id String Required The ID of the sensor you want to get readings from

Success Examples

Success Response

{
    "type": "success",
    "message": "get messages",
    "data": {
      "start": "2022-05-11 04:00:00Z",
      "end": "2022-05-12 03:59:59Z",
      "readings": [
        {
        "sensor_time": "2022-05-11 22:26:39Z",
        "temperature": 22.06 (Celsius),
        "humidity": 27.45,
        "offline": 0
        }
      ]
    }
  }
GET /sensors/all #

Get Sensors

Get all the sensors assigned to the account, *note: readings are shown in Celsius*

Group
Sensors
Version
1.0.0
Source
api\v1\index.php

Success Examples

Success Response

{
    "type": "success",
    "message": "get sensors",
    "data": {
      "groups": [],
      "items": [
        {
          "version": "",
          "sensor_id": "",
          "sensor_name": "",
          "sensor_mac_addr": "",
          "owner_id": "",
          "type": "",
          "alert_interval": "",
          "send_interval": "",
          "last_temp": 25.4 (Celsius),
          "last_humidity": 21.77,
          "last_voltage": "3.48",
          "battery_pct": 100,
          "wifi_connect_time": "1",
          "rssi": "-57",
          "last_checkin": "2021-10-25 20:04:25",
          "next_checkin": "2021-10-25 21:04:25",
          "ssid": "",
          "offline": "1",
          "group": 0,
          "connection_sensitivity": "3",
          "temp_offset": 0,
          "humidity_offset": 0
        }
      ]
    }
  }
GET /user #

Get Current User

Get your user information

Group
User
Version
1.0.0
Source
api\v1\index.php

Success Examples

Success Response

{
  "type": "success",
  "message": "Get user",
  "data": {
    "id": "",
    "email": "",
    "first_name": "",
    "last_name": "",
    "phone": "",
    "contact_email": "",
    "address_1": "",
    "address_2": "",
    "city": "",
    "state": "",
    "zip": "",
    "temp_pref": "F",
    "timezone": "America/New_York",
    "use_sensor_groups": "0",
    "chart_fill": "1",
    "send_reports": "1",
    "daily_reports": "1",
    "weekly_reports": "1",
    "monthly_reports": "1",
    "reports_specific_sensors": "1",
    "level": 10,
    "weekly_report_day": "1",
    "sensor_count": 1
  }
}
GET /user/allowed-timezones #

Get Timezones

Get current list of supported timezones

Group
User
Version
1.0.0
Source
api\v1\index.php

Success Examples

Success Response

{
  "type": "success",
  "message": "get timezones",
  "data": {
    "America/New_York": "US Eastern",
    "America/Chicago": "US Central",
    "America/Denver": "US Mountain West",
    "America/Phoenix": "US Mountain (No DST)",
    "America/Los_Angeles": "US Pacific",
    "America/Anchorage": "US Alaska",
    "Pacific/Honolulu": "US Hawaii",
    "America/Glace_Bay": "(GMT-04:00) Atlantic Time (Canada)",
    "America/Argentina/Buenos_Aires": "(GMT-03:00) Buenos Aires",
    "America/Noronha": "(GMT-02:00) Mid-Atlantic",
    "Atlantic/Azores": "(GMT-01:00) Azores",
    "Europe/London": "(GMT) London",
    "Europe/Berlin": "(GMT+01:00) Europe (CET)",
    "Africa/Algiers": "(GMT+01:00) West Africa",
    "Africa/Cairo": "(GMT+02:00) Central Africa",
    "Africa/Addis_Ababa": "(GMT+03:00) East Africa",
    "Europe/Moscow": "(GMT+03:00) Moscow",
    "Asia/Dubai": "(GMT+04:00) Abu Dhabi",
    "Asia/Karachi": "(GMT+05:00) Pakistan",
    "Asia/Calcutta": "(GMT+05:30) Calcutta",
    "Asia/Dhaka": "(GMT+06:00) Dhaka",
    "Asia/Bangkok": "(GMT+07:00) Bangkok",
    "Asia/Shanghai": "(GMT+08:00) Asia",
    "Asia/Tokyo": "(GMT+09:00) Tokyo",
    "Australia/Brisbane": "(GMT+10:00) Brisbane",
    "Asia/Magadan": "(GMT+11:00) Magadan",
    "Pacific/Auckland": "(GMT+12:00) Auckland",
    "Pacific/Tongatapu": "(GMT+13:00) Nuku alofa",
    "Pacific/Kiritimati": "(GMT+14:00) Kiritimati"
  }
}
POST /user/display-preferences #

Update User Display Preferences

Change display preferences including time zone and temperature format

Group
User
Version
1.0.0
Source
api\v1\index.php

Success Examples

Success Response

{
  "type": "success",
  "message": "updated display preferences",
  "data": {
    "timezone": "America/New_York",
    "temp_pref": "F",
    "chart_fill": "1"
  }
}
GET /user/email-reports #

Get Email Reports

Get email report settings and any downloadable reports

Group
User
Version
1.0.0
Source
api\v1\index.php

Success Examples

Success Response

{
  "type": "success",
  "message": "get email reports",
  "data": {
    "contacts": [],
    "sensors": [],
    "reports": {
      "daily": [
        {
          "report_id": "",
          "type": "daily",
          "file": "daily-2022-05-12.zip",
          "date_start": "2022-05-12 04:00:00",
          "date_end": "2022-05-13 03:59:59",
          "download": "URL_TO_DOWNLOAD"
        }
      ],
      "weekly": [],
      "monthly": []
    }
  }
}

Examples

Examples and Useful Code#

Battery Percentage Calculation#

At times, you may want to calculate the battery percentage of your sensor from the voltage sent by the sensor. This is

most accurate with alkaline batteries which have a steady voltage decline while lithium / rechargeable batteries tend to

hold voltage until failure.

const getBatteryPercentage = voltage => {

  // typical voltage range of sensors is 2.3v to 3.0v
  // (0% to 100%)

  const baseline = 2.30; // lowest allowed voltage
  const remaining_voltage = voltage - baseline;

  let voltagePct = remaining_voltage / 0.007;

  if(voltagePct > 100){ voltagePct = 100; }
  if(voltagePct < 0){ voltagePct = 0; }

  return Math.round(voltagePct);

}

Saving Readings and Alerts to Google Sheets#

This NodeJS script uses the Temp Stick API to fetch data from your sensors and update the readings while observing each

sensor's next check-in timestamp. Readings will be saved to the "Readings" tab of your sheet.

If any alerts are triggered, they will be recorded in the "Alerts" tab of your Google spreadsheet.

Setup#

  • Create a Service Account JSON key to access Google Sheets
  • Create a new Google Sheet (get the ID from the URL)
  • Create the tabs "Readings" and "Alerts"
  • "Readings" should have the following columns: "Sensor", "Temp C", "Temp F", "Humidity", "RSSI", "Time to Connect", "Battery", "Offline", "Timestamp"
  • "Alerts" should have the following columns: "Sensor", "Alert", "Timestamp"

To set up a service account for Google Services, go to this

guide for instructions

on obtaining a Service Key.


Keep in mind:

  • Your Google keyfile JSON is sensitive information, similar to a password to your Google account. Do not expose it publicly.
  • Similarly, your Temp Stick API key should be treated as confidential. It provides access to your Temp Stick account, hence don't include it directly in your code, especially if you're planning to share your code or upload it on platforms like Github. Always ensure your keys are securely stored and accessed.

Note

This is a basic example of integrating Google Sheets with Temp Stick. For a more robust application, you might want

to consider saving your readings and alerts to a dedicated database (such as SQL or MongoDB). Using a database could

provide more flexibility and efficiency than a spreadsheet for data management and retrieval.

This will not run in a web browser and requires NodeJS to use.


NodeJS Javascript

// Import necessary libraries
const {google} = require('googleapis'); // Google's Node.js client library
const axios = require('axios'); // HTTP client
const fs = require('fs'); // Node.js File System module

// Define the path for a JSON file where the application will store data. 
// The application will remember the last 1000 readings per sensor.
const dataFile = __dirname + '/data.json';

// Provide the API key for the Temp Stick service. This is reading a txt file called "temp-stick-api-key" (no extension)
const TEMP_STICK_API_KEY = fs.readFileSync(__dirname + "/temp-stick-api-key");

// Configure the Google JWT (JSON Web Token) authorization object
const auth = new google.auth.JWT({
  // Point to your Google service account file
  keyFile: __dirname + '/config/temp-stick-datalogging.json',
  // Specify the scope that the JWT is allowed to access. 
  // In this case, we're giving it access to Google Sheets.
  scopes: ['https://www.googleapis.com/auth/spreadsheets'],
});

// Initialize the Google Sheets API client
const sheets = google.sheets({version: 'v4', auth});

// Define the ID of the Google Sheets spreadsheet to be accessed. Replace '' with your actual spreadsheet id.
const spreadsheetId = '';

// Create a variable to store historical data. 
let dataHistory = null;

// Check if the file specified by dataFile already exists
if(fs.existsSync(dataFile)) {
  // If it does, read the file and parse its JSON content into the dataHistory variable
  dataHistory = JSON.parse(fs.readFileSync(dataFile));
} else {
  // If it doesn't, initialize dataHistory with an object that has an empty 'alerts' array
  dataHistory = {
    alerts: [],
  };
}

// Define a function to save the current state of dataHistory back into the dataFile
const saveData = () => {
  fs.writeFileSync(dataFile, JSON.stringify(dataHistory));
}

// The getSensors function retrieves sensor data from the Temp Stick API
async function getSensors() {
  try {
    // Send a GET request to the Temp Stick API's 'sensors/all' endpoint
    const response = await axios.get('https://tempstickapi.com/api/v1/sensors/all', {
      headers: {
        'X-API-KEY': TEMP_STICK_API_KEY // Include the Temp Stick API key in the request headers
      },
      timeout: 0, // Do not set a timeout for the request
    });

    const res = response.data; // Extract the data from the response

    // If the API request was successful, return the list of sensor items
    // If not, return false
    if(res.type === 'success') {
      return res.data.items;
    } else {
      return false;
    }

  } catch (error) { // If there's an error in the try block, log it
    console.error(error);
  }
}

// The getAlertNotifications function retrieves alert notifications from the Temp Stick API
async function getAlertNotifications() {
  try {
    // Send a GET request to the Temp Stick API's 'user/notifications' endpoint
    const response = await axios.get('https://tempstickapi.com/api/v1/user/notifications', {
      headers: {
        'X-API-KEY': TEMP_STICK_API_KEY // Include the Temp Stick API key in the request headers
      },
      timeout: 0, // Do not set a timeout for the request
    });

    const res = response.data; // Extract the data from the response

    // If the API request was successful, return the list of notification items
    // If not, return false
    if(res.type === 'success') {
      return res.data.items;
    } else {
      return false;
    }

  } catch (error) { // If there's an error in the try block, log it
    console.error(error);
  }
}

// The getMessages function retrieves messages for a specific sensor from the Temp Stick API
async function getMessages(sensorId) {
  try {
    // Send a GET request to the Temp Stick API's 'sensor/{sensorId}/readings' endpoint
    // The request will include a query string specifying the settings and offset for the readings
    const response = await axios.get(`https://tempstickapi.com/api/v1/sensor/${sensorId}/readings?setting=24_hours&offset=21600`, {
      headers: {
        'X-API-KEY': TEMP_STICK_API_KEY // Include the Temp Stick API key in the request headers
      },
      timeout: 0, // Do not set a timeout for the request
    });

    const res = response.data; // Extract the data from the response

    // If the API request was successful, return the list of readings
    // If not, return false
    if(res.type === 'success') {
      return res.data.readings;
    } else {
      return false;
    }

  } catch (error) { // If there's an error in the try block, log it
    console.error(error);
  }
}

// Function to add alerts to a Google Spreadsheet
async function addAlertsToSheet(alerts) {

  // Initialize an empty array to hold the data
  let data = [];

  // Loop through the alerts array
  for(alert of alerts) {

    // Combine the event type and event message into one string
    let message = alert.event_type + " " + alert.event_message;
    let created = alert.created;

    // Convert the created timestamp into Ontario time
    let ontarioTime = new Date(created).toLocaleString("en-US", {
      timeZone: "America/Toronto",
    });

    // Add a new row of data for each alert
    data.push([
      alert.sensor_id,
      message,
      ontarioTime,
    ])
  }

  // Sort the data array by date
  data = data.sort((a, b) => {
    return new Date(b[2]) - new Date(a[2]);
  });

  const sheetName = 'Alerts'; // replace with your sheet name

  // Get the spreadsheet details
  const spreadsheet = await sheets.spreadsheets.get({
    auth,
    spreadsheetId,
  });

  // Find the sheetId of the sheet with the given name
  const sheet = spreadsheet.data.sheets.find(sheet => sheet.properties.title === sheetName);
  const sheetId = sheet.properties.sheetId;

  // Insert rows into the spreadsheet
  await sheets.spreadsheets.batchUpdate({
    auth,
    spreadsheetId,
    resource: {
      requests: [{
        insertDimension: {
          range: {
            sheetId: sheetId,
            dimension: 'ROWS',
            startIndex: 1,
            endIndex: 1 + data.length
          },
          inheritFromBefore: false
        }
      }]
    }
  });

  // Update the spreadsheet with the new data
  await sheets.spreadsheets.values.update({
    auth,
    spreadsheetId,
    range: sheetName + '!A2', // assuming that your data starts from column A and the header is in the first row
    valueInputOption: 'USER_ENTERED',
    resource: { values: data },
  });

}

// Function to add sensor readings to a Google Spreadsheet
async function addSensorValuesToSheet(newReadings) {

  // Initialize an empty array to hold the data
  let data = [];

  // Loop through the readings array
  for(reading of newReadings) {

    // Grab the necessary values from the reading
    let tempC = reading.temperature;
    let tempF = (tempC * 9/5 + 32).toFixed(1); // Convert the temperature to Fahrenheit
    let humidity = reading.humidity;
    let checkin = reading.sensor_time;

    // Convert the check-in time to Ontario time
    let ontarioTime = new Date(checkin).toLocaleString("en-US", {
      timeZone: "America/Toronto",
    });

    let rssi = reading.rssi;
    let time_to_connect = reading.time_to_connect;
    let battery_pct = getBatteryPercentage(reading.voltage); // Get the battery percentage

    // Add a new row of data for each reading
    data.push([
      reading.sensor_id,
      tempC,
      tempF,
      humidity,
      rssi,
      time_to_connect,
      battery_pct,
      reading.offline,
      ontarioTime,
    ])
  }

  // Sort the data array by date
  data = data.sort((a, b) => {
    return new Date(b[8]) - new Date(a[8]);
  });

  const sheetName = 'Readings'; // replace with your sheet name

  // Insert rows into the spreadsheet
  await sheets.spreadsheets.batchUpdate({
    auth,
    spreadsheetId,
    resource: {
      requests: [{
        insertDimension: {
          range: {
            sheetId: 0,
            dimension: 'ROWS',
            startIndex: 1,
            endIndex: 1 + data.length
          },
          inheritFromBefore: false
        }
      }]
    }
  });

  // Update the spreadsheet with the new data
  await sheets.spreadsheets.values.update({
    auth,
    spreadsheetId,
    range: sheetName + '!A2', // assuming that your data starts from column A and the header is in the first row
    valueInputOption: 'USER_ENTERED',
    resource: { values: data },
  });

}

// Function to process the alerts
const processAlerts = async () => {
  // Fetch alert notifications
  const alerts = await getAlertNotifications();

  // Initialize an empty array for new alerts
  const newAlerts = [];

  // Loop over all alerts
  for(let alert of alerts) {
    // Check if the alert already exists in the history
    let alertExists = false;
    for(let existingAlert of dataHistory.alerts) {
      if(existingAlert.id === alert.id) {
        alertExists = true;
        break;
      }
    }

    // If the alert doesn't exist, add it to the new alerts array
    if (!alertExists) {
      newAlerts.unshift(alert);
    }
  }

  // Add new alerts to the history and limit history size to 1000 entries
  dataHistory.alerts = [...newAlerts, ...dataHistory.alerts];
  if(dataHistory.alerts.length > 1000) {
    dataHistory.alerts = dataHistory.alerts.slice(0, 1000);
  }

  // Add new alerts to the Google Sheet and save data
  await addAlertsToSheet(newAlerts);
  saveData();
};

// Function to process sensor data
const processSensors = async () => {
  // Fetch sensors data
  const sensors = await getSensors();

  // Process alerts after 60 seconds to allow for data to come in
  setTimeout(processAlerts, 60000);

  // If sensors are available
  if (sensors && sensors.length > 0) {
    let nextCheckIn = null;
    const now = new Date();
    const batchUpdate = [];

    // Loop over all sensors
    for (let sensor of sensors) {

      // If there's no history for this sensor, create a new array for it
      if(!dataHistory[sensor.sensor_id]) {
        dataHistory[sensor.sensor_id] = [];
      }

      // Get messages for each sensor
      const readings = await getMessages(sensor.sensor_id);
      const newReadings = [];

      // Loop over all readings
      for(let reading of readings) {
        reading.sensor_id = sensor.sensor_id;
        let readingExists = false;

        // Check if the reading already exists in the history
        for(let existingReading of dataHistory[sensor.sensor_id]) {
          if(existingReading.sensor_time === reading.sensor_time) {
            readingExists = true;
            break;
          }
        }

        // If the reading doesn't exist, add it to the new readings array and batch update array
        if (!readingExists) {
          newReadings.unshift(reading);
          batchUpdate.push(reading);
        }
      }

      // Add new readings to the history and limit history size to 1000 entries
      dataHistory[sensor.sensor_id] = [...newReadings, ...dataHistory[sensor.sensor_id]];
      if(dataHistory[sensor.sensor_id].length > 1000) {
        dataHistory[sensor.sensor_id] = dataHistory[sensor.sensor_id].slice(0, 1000);
      }

      // Calculate the next check-in time
      let sensorNextCheckIn = new Date(sensor.next_checkin + "Z");
      while (sensorNextCheckIn <= now) {
        sensorNextCheckIn = new Date(sensorNextCheckIn.getTime() + 60000);
      }
      if (!nextCheckIn || sensorNextCheckIn < nextCheckIn) {
        nextCheckIn = sensorNextCheckIn;
      }
    }

    // If there are any new readings, add them to the Google Sheet
    if(batchUpdate.length > 0) {
      await addSensorValuesToSheet(batchUpdate);
    }

    // Save the data
    saveData();

    // Calculate sleep time until the next check-in
    if (nextCheckIn) {
      let sleepTime = nextCheckIn.getTime() - now.getTime() + 60 * 1000;
      if(sleepTime < 0) {
        sleepTime = 15000;
      }

      // Delay the next sensor process by sleep time
      if (sleepTime > 0) {
        console.log(`Sleeping for ${sleepTime / 1000} seconds...`);
        setTimeout(processSensors, sleepTime);
      } else {
        // always sleep for 60 seconds although this should never process
        setTimeout(processSensors, 60000);
      }
    } else {
      console.log('No next check-in time. Exiting...');
    }
  } else {
    console.log('No sensors. Exiting...');
  }
};

// Start processing the sensors
processSensors();