☀️ Kühle dein Gewächshaus - 5 % Rabatt auf Kits — CODE: WINTER5
GEIA Official Guide Custom Sensor

Add a Custom Sensor to GEIA Custom Sensor

Connect an unsupported sensor with your own ESP32 or ESP8266 firmware, then send live or logged data to GEIA using MQTT or the REST API.

🌳 Expert ⏱ 45–90 minutes Open Updated Sep. 15, 2026
Maintained by GEIA

In this guide

🧩 Sensors 🌳 Expert ⏱ 45–90 minutes
Overview

Add a Custom Sensor to GEIA

Connect an unsupported sensor with your own ESP32 or ESP8266 firmware, then send live or logged data to GEIA using MQTT or the REST API.

Steps

Step-by-step

01

What you need

Use this guide when your sensor is not supported by GEIA firmware and you want to read it with your own code. Your device will send the readings to GEIA by MQTT or the REST API.

Programmable board An ESP32, ESP8266 or another controller that can connect to Wi-Fi.
Sensor + details The sensor, its datasheet, wiring information and a working code library.
Build tools A computer, USB data cable and Arduino IDE or your preferred toolchain.
GEIA access A GEIA account, app dashboard, Wi-Fi and API or MQTT credentials.
Before you code: Check the sensor voltage and pin requirements. Do not connect a 5V signal directly to a 3.3V GPIO unless the board or a level shifter makes it safe.

The supplied sketches use ESP32 headers. For ESP8266, change the Wi-Fi and HTTPS includes for the ESP8266 Arduino core. If your sensor is already listed, use the standard compatible sensor setup instead.

02

Get API & MQTT Access

Both account types receive API access during signup. Choose whether you want a live farming dashboard or a test dashboard for development.

Grower path

Sign up as a farmer

Choose this for a real grow. Signup creates your live app dashboard and provides API credentials.

Create a farmer account

Developer path

Join as an Ecosystem developer

Choose this for development and integration testing. Select Ecosystem Partner → Developer → Generate API Access to receive API credentials and a test dashboard.

Generate developer access

The live and test dashboards currently use nearly the same interface. Choose the account by its purpose: real farm operation or development and testing.

Save these details privately:
  • API access token (JWT) and refresh token
  • Assigned API server or cluster address
  • MQTT host and authentication details
  • Location ID
Use access token Token expires / API returns 401 Call the refresh route Save and use the new token

The REST access token expires. Your code must renew it with the token-refresh route and safely save any replacement token returned by the server.

03

Add a Virtual Node & Custom Sensor

Create the app records before uploading your custom firmware. This gives your code the correct node, sensor and location IDs.

1

Add or choose a node

For a board running only your own firmware, add a node and select Virtual Node. If you already created the correct custom node, choose it.

2

Add the sensor

Open Add Sensor. Enter a name, choose the node and category, then select Custom Sensor as the Type.

3

Save the IDs

Finish the wizard, then copy the location ID, node ID, sensor ID and MQTT topic shown in the app.

Expected difference: Pin/GPIO, refresh-rate and data-record-rate settings may not appear for a Custom Sensor using custom firmware. Your own code controls when it reads and sends data.

You can use the web app, but the node and gateway must be connected to the internet while you complete cloud setup.

04

Send Live Data with MQTT

Use local MQTT for live readings from a device that stays connected. Publish to the Grow Hub; when cloud sync is enabled, the Hub forwards the data to GEIA Cloud.

Expected sensor topic

Publish on the local topic

e/s/SENSOR_ID

Replace SENSOR_ID with the ID shown in the GEIA app. Do not add a location prefix for a local Grow Hub connection.

Grow Hub sync

Keep one local connection

Sensor → Grow Hub → GEIA Cloud

The custom node publishes locally. The Grow Hub handles cloud synchronization when it is connected and sync is enabled.

Read sensor Build value payload Publish to sensor topic View live value in GEIA
  • Replace every placeholder in the example with your own values.
  • Replace the example sensor-reading functions with calls from your sensor library.
  • The example sends two space-separated values. Use the payload and value order configured for your sensor.
  • Use a non-blocking timer so MQTT can stay connected.

Check the current MQTT documentation for your assigned host, port, topic and payload details.

Step 04 code · ARDUINO
/*
  GEIA custom sensor - MQTT example
  Target: ESP32
  Library: PubSubClient

  Replace every YOUR_... value before uploading.
  Replace readSensorValue1() and readSensorValue2() with your sensor library calls.
  Never publish real credentials in a public repository.
*/

#include <WiFi.h>
#include <PubSubClient.h>
#include <math.h>

// Wi-Fi
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";

// MQTT details supplied by GEIA or shown on the Grow Hub page
const char* MQTT_HOST = "YOUR_ASSIGNED_MQTT_HOST";
const uint16_t MQTT_PORT = 1883;
const char* MQTT_USER = "YOUR_MQTT_USERNAME_OR_TOKEN";
const char* MQTT_PASSWORD = "YOUR_MQTT_PASSWORD";

// Use the location ID for a cloud connection.
// Leave LOCATION_ID empty ("") for a local Grow Hub connection.
const char* LOCATION_ID = "YOUR_LOCATION_ID";
const char* SENSOR_ID = "YOUR_SENSOR_ID";

const unsigned long PUBLISH_EVERY_MS = 30000;
const float MINIMUM_CHANGE = 0.1f;

WiFiClient networkClient;
PubSubClient mqtt(networkClient);

unsigned long lastPublishAt = 0;
unsigned long lastMqttAttemptAt = 0;
float lastValue1 = NAN;
float lastValue2 = NAN;

// Replace these two functions with real sensor library calls.
float readSensorValue1() {
  return 22.5f;
}

float readSensorValue2() {
  return 55.1f;
}

String sensorTopic() {
  String topic = "e/s/" + String(SENSOR_ID);

  if (strlen(LOCATION_ID) > 0) {
    topic = "/" + String(LOCATION_ID) + "/" + topic;
  }

  return topic;
}

void connectWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  Serial.print("Connecting to Wi-Fi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print('.');
  }
  Serial.println(" connected");
}

void maintainMqtt() {
  if (mqtt.connected()) {
    return;
  }

  const unsigned long now = millis();
  if (now - lastMqttAttemptAt < 5000) {
    return;
  }
  lastMqttAttemptAt = now;

  String clientId = "GEIA-CustomSensor-" + String(random(0xffff), HEX);
  Serial.print("Connecting to MQTT...");

  if (mqtt.connect(clientId.c_str(), MQTT_USER, MQTT_PASSWORD)) {
    Serial.println(" connected");
  } else {
    Serial.print(" failed, state=");
    Serial.println(mqtt.state());
  }
}

void publishSensorReading() {
  if (!mqtt.connected()) {
    return;
  }

  const float value1 = readSensorValue1();
  const float value2 = readSensorValue2();

  if (isnan(value1) || isnan(value2)) {
    Serial.println("Sensor returned an invalid value");
    return;
  }

  const bool firstReading = isnan(lastValue1) || isnan(lastValue2);
  const bool changed = fabsf(value1 - lastValue1) >= MINIMUM_CHANGE ||
                       fabsf(value2 - lastValue2) >= MINIMUM_CHANGE;

  if (!firstReading && !changed) {
    Serial.println("Reading has not changed enough; not publishing");
    return;
  }

  // This example sends two space-separated values: "value_1 value_2".
  // For a one-value sensor, use only String(value1, 2).
  String payload = String(value1, 2) + " " + String(value2, 2);
  String topic = sensorTopic();

  if (mqtt.publish(topic.c_str(), payload.c_str(), true)) {
    Serial.println("Published " + payload + " to " + topic);
    lastValue1 = value1;
    lastValue2 = value2;
  } else {
    Serial.println("MQTT publish failed");
  }
}

void setup() {
  Serial.begin(115200);
  randomSeed(micros());
  connectWiFi();
  mqtt.setServer(MQTT_HOST, MQTT_PORT);
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    connectWiFi();
  }

  maintainMqtt();
  mqtt.loop();

  const unsigned long now = millis();
  if (now - lastPublishAt >= PUBLISH_EVERY_MS) {
    lastPublishAt = now;
    publishSensorReading();
  }
}
05

Log Data with the REST API

The full example uses both paths: MQTT broadcasts the current reading, while the local API records historical data. The Grow Hub can then synchronize those records with GEIA Cloud.

Local logging

Record the sensor data

POST {local_api_base}/data/add/sensor

Send the sensor ID and its value fields as JSON with the current API access token.

Renew access

Refresh an expired token

POST {local_api_base}/auth/refresh

If the data request returns 401, send the refresh token, save the new access token and retry the reading once.

Check before publishing: API base addresses, routes and response fields can differ by cluster or API version. Confirm them in the current documentation and in the credentials supplied to your account.
  • The complete sketch reads the sensor once and sends the same values to MQTT and the API.
  • MQTT uses its own username/password authentication; API logging uses a Bearer access token.
  • For a remote API connection, use HTTPS and certificate validation.
  • Do not put API secrets in a public repository or client-side webpage.
  • Store renewed tokens securely if the device must survive a restart.
  • Choose an upload interval allowed by your PaaS package.

Check the current REST API documentation before deploying the device.

Step 05 code · ARDUINO
/*
  GEIA custom sensor - REST API logging example
  Target: ESP32
  Libraries: HTTPClient and ArduinoJson

  This starter example:
  1. reads two sensor values;
  2. sends them to the GEIA sensor-data route;
  3. refreshes the JWT after a 401 response; and
  4. retries the reading once.

  Confirm the API base, routes and JSON fields for your assigned cluster.
  Add current TLS certificate validation before production deployment.
  Never publish real credentials in a public repository.
*/

#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <math.h>

// Wi-Fi
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";

// Use the API base supplied with your credentials.
const char* API_BASE = "https://api.geia.ai";
const char* SENSOR_LOG_ROUTE = "/data/add/sensor";
const char* TOKEN_REFRESH_ROUTE = "/auth/refresh";

const char* SENSOR_ID = "YOUR_SENSOR_ID";
String accessToken = "YOUR_INITIAL_JWT_ACCESS_TOKEN";
String refreshToken = "YOUR_REFRESH_TOKEN";

const unsigned long UPLOAD_EVERY_MS = 300000;
unsigned long lastUploadAt = 0;

// Replace these two functions with real sensor library calls.
float readSensorValue1() {
  return 22.5f;
}

float readSensorValue2() {
  return 55.1f;
}

void connectWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  Serial.print("Connecting to Wi-Fi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print('.');
  }
  Serial.println(" connected");
}

// Replace this function with secure NVS/Preferences storage if tokens must
// survive a restart. Some servers may also rotate the refresh token.
void saveRenewedTokens() {
  // Example: save accessToken and refreshToken with Preferences.
}

int postSensorPayload(const String& payload) {
  HTTPClient http;
  const String url = String(API_BASE) + SENSOR_LOG_ROUTE;

  if (!http.begin(url)) {
    Serial.println("Could not start HTTPS request");
    return -1;
  }

  http.addHeader("Content-Type", "application/json");
  http.addHeader("Authorization", "Bearer " + accessToken);
  http.setTimeout(15000);

  const int status = http.POST(payload);
  const String body = http.getString();

  Serial.print("Sensor API status: ");
  Serial.println(status);
  if (status < 200 || status >= 300) {
    Serial.println(body);
  }

  http.end();
  return status;
}

bool refreshAccessToken() {
  HTTPClient http;
  const String url = String(API_BASE) + TOKEN_REFRESH_ROUTE;

  if (!http.begin(url)) {
    Serial.println("Could not start token-refresh request");
    return false;
  }

  http.addHeader("Content-Type", "application/json");
  http.setTimeout(15000);

  StaticJsonDocument<256> request;
  request["refresh_token"] = refreshToken;

  String payload;
  serializeJson(request, payload);

  const int status = http.POST(payload);
  const String body = http.getString();
  http.end();

  if (status < 200 || status >= 300) {
    Serial.print("Token refresh failed, status: ");
    Serial.println(status);
    Serial.println(body);
    return false;
  }

  StaticJsonDocument<768> response;
  const DeserializationError jsonError = deserializeJson(response, body);
  if (jsonError) {
    Serial.print("Could not read token response: ");
    Serial.println(jsonError.c_str());
    return false;
  }

  String newAccessToken;
  if (!response["jwt"].isNull()) {
    newAccessToken = response["jwt"].as<String>();
  } else if (!response["token"].isNull()) {
    newAccessToken = response["token"].as<String>();
  } else if (!response["data"]["jwt"].isNull()) {
    newAccessToken = response["data"]["jwt"].as<String>();
  } else if (!response["data"]["token"].isNull()) {
    newAccessToken = response["data"]["token"].as<String>();
  }

  if (newAccessToken.length() == 0) {
    Serial.println("Refresh response did not contain a new access token");
    return false;
  }

  accessToken = newAccessToken;

  if (!response["refresh_token"].isNull()) {
    refreshToken = response["refresh_token"].as<String>();
  } else if (!response["data"]["refresh_token"].isNull()) {
    refreshToken = response["data"]["refresh_token"].as<String>();
  }

  saveRenewedTokens();
  Serial.println("Access token refreshed");
  return true;
}

bool uploadSensorReading(float value1, float value2) {
  StaticJsonDocument<256> document;
  document["sensor_id"] = SENSOR_ID;
  document["value_1"] = value1;
  document["value_2"] = value2;

  // Leave sample_timestamp out if the server should set the time.
  String payload;
  serializeJson(document, payload);

  int status = postSensorPayload(payload);
  if (status == 401) {
    Serial.println("Access token expired; refreshing it now");
    if (!refreshAccessToken()) {
      return false;
    }
    status = postSensorPayload(payload);  // Retry only once.
  }

  return status >= 200 && status < 300;
}

void setup() {
  Serial.begin(115200);
  connectWiFi();
  lastUploadAt = millis() - UPLOAD_EVERY_MS;  // Send the first reading now.
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    connectWiFi();
  }

  const unsigned long now = millis();
  if (now - lastUploadAt < UPLOAD_EVERY_MS) {
    return;
  }
  lastUploadAt = now;

  const float value1 = readSensorValue1();
  const float value2 = readSensorValue2();

  if (isnan(value1) || isnan(value2)) {
    Serial.println("Sensor returned an invalid value");
    return;
  }

  if (uploadSensorReading(value1, value2)) {
    Serial.println("Sensor reading uploaded");
  } else {
    Serial.println("Sensor reading was not uploaded");
  }
}
06

Test Your Sensor & Know the Limits

Keep the Serial Monitor open for the first test. Confirm the raw reading, the connection result and the value shown in the GEIA app.

Test checklist

Confirm the full path

  • The sensor returns a sensible raw value.
  • MQTT connects or the REST request succeeds.
  • The app shows the value under the correct sensor.
  • Units and value order are correct.
  • New readings appear at the expected interval.
Common failures

If no data appears

  • Check the API cluster or MQTT hostname.
  • Check the location ID, sensor ID and topic slashes.
  • Check the access token, refresh token or MQTT login.
  • Check that your payload uses the expected value order.
  • If REST keeps returning 401, check the refresh response and save the new token.
Automation limit: Most unsupported Custom Sensors are for monitoring and logging. They normally cannot be selected in standard automation functions until their hardware and data mapping are reviewed. Where available, use them with Rules or Safety Automation functions and add independent physical fail-safes.

Need full platform support? Request sensor compatibility and include the sensor model, datasheet, interface, voltage, example code and intended use.

Next guide: Build a custom coded automation using sensor data, relay state checks and safe hysteresis.

07

Full Code here

Download full .ino code

Remember to adjust settings for your setup and account.

Automation safety note

Keep Wi-Fi passwords, API tokens and MQTT credentials private. Never paste real credentials into public code or screenshots. Test sensor values before using them in any rule, and do not use an unapproved custom sensor to control critical equipment. Use independent limits and physical fail-safes for pumps, heaters, dosing, CO₂ and other equipment that could harm plants, people or property.

Start typing and press Enter to search

Shopping Cart

Es befinden sich keine Produkte im Warenkorb.

de_DE